draftgo-cli 3.0.1 → 3.0.33
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/LICENSE +21 -0
- package/README.md +73 -124
- package/package.json +21 -8
- package/resources/skill/SKILL.md +62 -89
- package/resources/skill/init/SKILL.md +18 -67
- package/resources/skill/manifest.json +27 -0
- package/resources/skill/pull/SKILL.md +18 -44
- package/resources/skill/push/SKILL.md +30 -247
- package/resources/skill/references/aihub.md +86 -0
- package/resources/skill/references/api-endpoints.md +178 -0
- package/resources/skill/references/api.json +20248 -0
- package/resources/skill/{quickref → references}/app-api.md +44 -14
- package/resources/skill/{core → references}/architecture.md +6 -26
- package/resources/skill/references/chat-sdk.md +201 -0
- package/resources/skill/references/custom-services.md +308 -0
- package/resources/skill/references/data.md +298 -0
- package/resources/skill/references/db-relations.md +227 -0
- package/resources/skill/references/frontend.md +788 -0
- package/resources/skill/references/modules.md +66 -0
- package/resources/skill/references/parallel.md +48 -0
- package/resources/skill/{specs → references}/runtime.md +31 -1
- package/resources/skill/{specs → references}/security.md +3 -3
- package/resources/skill/references/ui-protocol.md +99 -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 +440 -183
- package/resources/skill/story/SKILL.md +13 -23
- package/src/cli.js +22 -7
- package/src/commandRegistry.js +34 -0
- package/src/commands/api.js +204 -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/help.js +41 -28
- package/src/commands/init.js +34 -20
- package/src/commands/local.js +9 -3
- package/src/commands/map.js +18 -7
- package/src/commands/sync.js +11 -4
- package/src/commands/update.js +39 -52
- package/src/commands/verifyUi.js +199 -0
- package/src/index.js +13 -46
- package/src/localdev/compose.js +48 -197
- package/src/localdev/index.js +116 -216
- package/src/localdev/mysqlClient.js +12 -9
- package/src/localdev/services.js +163 -0
- package/src/platforms.js +3 -3
- package/src/projectConfig.js +12 -2
- package/src/projectMap.js +240 -68
- package/src/skill.js +113 -29
- package/src/updateCheck.js +37 -15
- package/resources/skill/core/modules.md +0 -54
- package/resources/skill/practices/anti-patterns.md +0 -70
- package/resources/skill/practices/best-practices.md +0 -41
- package/resources/skill/practices/dev-declaration.md +0 -94
- package/resources/skill/quickref/api-endpoints.md +0 -130
- package/resources/skill/quickref/api.json +0 -17675
- package/resources/skill/quickref/dg-components.md +0 -198
- package/resources/skill/rules/dev-workflow.md +0 -652
- package/resources/skill/rules/frontend.md +0 -210
- package/resources/skill/rules/parallel.md +0 -263
- package/resources/skill/specs/data.md +0 -108
- package/resources/skill/specs/ui-protocol.md +0 -68
- package/src/commands/doctor.js +0 -54
- package/src/commands/new.js +0 -183
- package/src/commands/projectScript.js +0 -37
- /package/resources/skill/{rules → references}/debugging-syntax.md +0 -0
|
@@ -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,21 @@ 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
|
-
import json, sys, re
|
|
20
|
+
import json, sys, re, datetime
|
|
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
|
|
27
|
+
RUN_FAILURES = 0
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def find_project_root(start: Path) -> Path:
|
|
@@ -43,46 +45,6 @@ def find_project_root(start: Path) -> Path:
|
|
|
43
45
|
|
|
44
46
|
DEFAULT_ROOT = find_project_root(SCRIPT_DIR)
|
|
45
47
|
|
|
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
48
|
def load_config():
|
|
87
49
|
cfg_path = DEFAULT_ROOT / ".draftgo/config.json"
|
|
88
50
|
if not cfg_path.exists():
|
|
@@ -93,6 +55,8 @@ def load_config():
|
|
|
93
55
|
|
|
94
56
|
|
|
95
57
|
def _lessons_reminder(cfg):
|
|
58
|
+
if DRY_RUN:
|
|
59
|
+
return
|
|
96
60
|
if not cfg.get("lessons_on_push", False):
|
|
97
61
|
return
|
|
98
62
|
print("")
|
|
@@ -104,7 +68,9 @@ def _lessons_reminder(cfg):
|
|
|
104
68
|
print(" 未命中则跳过。")
|
|
105
69
|
|
|
106
70
|
|
|
107
|
-
def api_call(method, server, token, path, body=None):
|
|
71
|
+
def api_call(method, server, token, path, body=None, acceptable_errors=()):
|
|
72
|
+
"""发起 API 调用,并记录调用方不会恢复的失败。"""
|
|
73
|
+
global RUN_FAILURES
|
|
108
74
|
data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None
|
|
109
75
|
headers = {
|
|
110
76
|
"Authorization": f"Bearer {token}",
|
|
@@ -126,8 +92,12 @@ def api_call(method, server, token, path, body=None):
|
|
|
126
92
|
with urllib.request.urlopen(req, timeout=15) as r:
|
|
127
93
|
return True, r.status, r.read().decode("utf-8", errors="replace")
|
|
128
94
|
except urllib.error.HTTPError as e:
|
|
129
|
-
|
|
95
|
+
body_text = e.read().decode(errors="replace")
|
|
96
|
+
if e.code not in acceptable_errors:
|
|
97
|
+
RUN_FAILURES += 1
|
|
98
|
+
return False, e.code, body_text
|
|
130
99
|
except Exception as e:
|
|
100
|
+
RUN_FAILURES += 1
|
|
131
101
|
return False, None, str(e)
|
|
132
102
|
|
|
133
103
|
|
|
@@ -159,8 +129,9 @@ def _extract_created_id(body_text):
|
|
|
159
129
|
def _writeback_index(rel_index, items):
|
|
160
130
|
"""将更新后的 index 列表写回 .draftgo/ 下的 index.json。"""
|
|
161
131
|
path = DEFAULT_ROOT / ".draftgo" / rel_index
|
|
162
|
-
if not path.exists():
|
|
132
|
+
if not path.exists() and not (DEFAULT_ROOT / ".draftgo").exists():
|
|
163
133
|
path = DEFAULT_ROOT / rel_index
|
|
134
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
164
135
|
path.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
165
136
|
|
|
166
137
|
|
|
@@ -185,22 +156,247 @@ def _rename_to_canonical(sub_dir, old_rel, prefix, new_id, slug_src, ext):
|
|
|
185
156
|
return old_rel
|
|
186
157
|
|
|
187
158
|
|
|
188
|
-
def _load_index(rel_index):
|
|
159
|
+
def _load_index(rel_index, allow_missing=False):
|
|
189
160
|
"""读取 .draftgo/ 下的 index.json;支持旧 (pages/index.json) 与新 (.draftgo/pages/index.json) 两种布局。"""
|
|
190
161
|
new_path = DEFAULT_ROOT / ".draftgo" / rel_index
|
|
191
162
|
legacy_path = DEFAULT_ROOT / rel_index
|
|
192
163
|
path = new_path if new_path.exists() else legacy_path
|
|
193
164
|
if not path.exists():
|
|
165
|
+
if allow_missing:
|
|
166
|
+
print(f" WARN {rel_index} 不存在,将尝试从云端恢复指定目标的索引")
|
|
167
|
+
return []
|
|
194
168
|
print(f"ERR: {rel_index} not found under .draftgo/, run /draftgo init first", file=sys.stderr)
|
|
195
169
|
sys.exit(1)
|
|
196
170
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
197
171
|
|
|
198
172
|
|
|
173
|
+
def _filter_items_by_ids(items, ids, id_key, type_name, orphan=None):
|
|
174
|
+
"""按用户指定 id 过滤;指定 id 不存在时直接失败,避免 0 目标假成功。"""
|
|
175
|
+
if not ids:
|
|
176
|
+
return items
|
|
177
|
+
wanted = [str(x) for x in ids]
|
|
178
|
+
found = {str(it.get(id_key)) for it in items if it.get(id_key) is not None}
|
|
179
|
+
missing = [x for x in wanted if x not in found]
|
|
180
|
+
if missing:
|
|
181
|
+
print(f"ERR: {type_name} index.json 未登记目标 {id_key}: {', '.join(missing)}", file=sys.stderr)
|
|
182
|
+
if orphan:
|
|
183
|
+
hints = orphan(missing)
|
|
184
|
+
if hints:
|
|
185
|
+
print(" 发现疑似未入索引的本地文件:", file=sys.stderr)
|
|
186
|
+
for hint in hints[:8]:
|
|
187
|
+
print(f" - {hint}", file=sys.stderr)
|
|
188
|
+
print(" 请先恢复/追加对应 index 元数据,或重新运行 pull 后再 push。", file=sys.stderr)
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
return [it for it in items if str(it.get(id_key)) in wanted]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _response_items(body):
|
|
194
|
+
"""解析列表接口的常见统一信封格式。"""
|
|
195
|
+
try:
|
|
196
|
+
raw = json.loads(body)
|
|
197
|
+
except Exception:
|
|
198
|
+
return []
|
|
199
|
+
if isinstance(raw, dict) and "data" in raw:
|
|
200
|
+
raw = raw["data"]
|
|
201
|
+
if isinstance(raw, list):
|
|
202
|
+
return raw
|
|
203
|
+
if isinstance(raw, dict):
|
|
204
|
+
for key in ("items", "results", "data"):
|
|
205
|
+
if isinstance(raw.get(key), list):
|
|
206
|
+
return raw[key]
|
|
207
|
+
return [raw]
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _response_item(body):
|
|
212
|
+
"""解析单资源响应的统一信封。"""
|
|
213
|
+
try:
|
|
214
|
+
raw = json.loads(body)
|
|
215
|
+
except Exception:
|
|
216
|
+
return None
|
|
217
|
+
if isinstance(raw, dict) and "data" in raw:
|
|
218
|
+
raw = raw["data"]
|
|
219
|
+
return raw if isinstance(raw, dict) else None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _cloud_version_allows_push(server, token, path, local_item, label):
|
|
223
|
+
"""云端优先:本地 pull 基线落后于云端时禁止静默覆盖。
|
|
224
|
+
|
|
225
|
+
没有历史 updated_at 的旧缓存保持兼容;404 由调用方按默认重建策略处理。
|
|
226
|
+
"""
|
|
227
|
+
global RUN_FAILURES
|
|
228
|
+
baseline = local_item.get("updated_at")
|
|
229
|
+
if not baseline:
|
|
230
|
+
return True
|
|
231
|
+
ok, status, body = api_call("GET", server, token, path, acceptable_errors=(404,))
|
|
232
|
+
if not ok and status == 404:
|
|
233
|
+
return True
|
|
234
|
+
if not ok:
|
|
235
|
+
return False
|
|
236
|
+
remote = _response_item(body)
|
|
237
|
+
remote_updated = remote.get("updated_at") if remote else None
|
|
238
|
+
if remote_updated and str(remote_updated) != str(baseline):
|
|
239
|
+
RUN_FAILURES += 1
|
|
240
|
+
conflict_dir = DEFAULT_ROOT / ".draftgo" / "sync-conflicts"
|
|
241
|
+
conflict_dir.mkdir(parents=True, exist_ok=True)
|
|
242
|
+
identity = str(local_item.get("id") or label).replace("/", "_").replace("\\", "_")
|
|
243
|
+
stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
244
|
+
conflict_path = conflict_dir / f"{identity}-{stamp}.json"
|
|
245
|
+
conflict_path.write_text(json.dumps({
|
|
246
|
+
"kind": "remote-newer",
|
|
247
|
+
"resource_path": path,
|
|
248
|
+
"baseline_updated_at": baseline,
|
|
249
|
+
"remote_updated_at": remote_updated,
|
|
250
|
+
"local": local_item,
|
|
251
|
+
"remote": remote,
|
|
252
|
+
}, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
|
|
253
|
+
print(
|
|
254
|
+
f" ERR [{label}] 云端版本已变化(local={baseline}, remote={remote_updated});"
|
|
255
|
+
f"云端优先,已保存冲突快照:{conflict_path.relative_to(DEFAULT_ROOT)}。",
|
|
256
|
+
file=sys.stderr,
|
|
257
|
+
)
|
|
258
|
+
return False
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _recover_missing_index_entries(
|
|
263
|
+
server, token, items, ids, id_key, type_name, rel_index, endpoint,
|
|
264
|
+
file_spec=None, drop_fields=(),
|
|
265
|
+
):
|
|
266
|
+
"""补齐缺失的 index 条目,且绝不覆盖本地受管内容。
|
|
267
|
+
|
|
268
|
+
只有显式指定的目标才可恢复,防止 `push --all` 把不明文件自动纳管。
|
|
269
|
+
file_spec 为 (目录, index 文件字段, 文件名前缀, 扩展名列表)。
|
|
270
|
+
"""
|
|
271
|
+
if not ids:
|
|
272
|
+
return items
|
|
273
|
+
wanted = [str(value) for value in ids]
|
|
274
|
+
registered = {str(item.get(id_key)) for item in items if item.get(id_key) is not None}
|
|
275
|
+
missing = [value for value in wanted if value not in registered]
|
|
276
|
+
if not missing:
|
|
277
|
+
return items
|
|
278
|
+
if DRY_RUN:
|
|
279
|
+
print(
|
|
280
|
+
f"ERR: {type_name} index.json 未登记目标 {id_key}: {', '.join(missing)}。"
|
|
281
|
+
"dry-run 不会回读云端或修改本地索引。",
|
|
282
|
+
file=sys.stderr,
|
|
283
|
+
)
|
|
284
|
+
sys.exit(1)
|
|
285
|
+
|
|
286
|
+
ok, status, body = api_call("GET", server, token, endpoint)
|
|
287
|
+
if not ok:
|
|
288
|
+
print(
|
|
289
|
+
f"ERR: 无法恢复 {type_name} index.json:云端回读失败 -> {_info(ok, status, body)}",
|
|
290
|
+
file=sys.stderr,
|
|
291
|
+
)
|
|
292
|
+
sys.exit(1)
|
|
293
|
+
remote_by_id = {
|
|
294
|
+
str(item.get(id_key)): item
|
|
295
|
+
for item in _response_items(body)
|
|
296
|
+
if isinstance(item, dict) and item.get(id_key) is not None
|
|
297
|
+
}
|
|
298
|
+
absent = [value for value in missing if value not in remote_by_id]
|
|
299
|
+
if absent:
|
|
300
|
+
print(
|
|
301
|
+
f"ERR: {type_name} 云端不存在目标 {id_key}: {', '.join(absent)}。"
|
|
302
|
+
"本地资源可能已成为孤儿文件,请确认后删除本地文件及其引用,"
|
|
303
|
+
"或改为新建资源推送。",
|
|
304
|
+
file=sys.stderr,
|
|
305
|
+
)
|
|
306
|
+
if file_spec:
|
|
307
|
+
rel_dir, _, prefix, exts = file_spec
|
|
308
|
+
hints = _orphan_hints(rel_dir, prefix, absent, exts)
|
|
309
|
+
for hint in hints:
|
|
310
|
+
print(f" 建议删除:{hint}", file=sys.stderr)
|
|
311
|
+
sys.exit(1)
|
|
312
|
+
|
|
313
|
+
recovered = []
|
|
314
|
+
for value in missing:
|
|
315
|
+
meta = dict(remote_by_id[value])
|
|
316
|
+
if file_spec:
|
|
317
|
+
rel_dir, file_field, prefix, exts = file_spec
|
|
318
|
+
candidates = _orphan_hints(rel_dir, prefix, [value], exts)
|
|
319
|
+
if len(candidates) != 1:
|
|
320
|
+
detail = "未找到" if not candidates else f"找到 {len(candidates)} 个"
|
|
321
|
+
print(
|
|
322
|
+
f"ERR: {type_name} {id_key}={value} 云端存在,但本地孤儿文件{detail},"
|
|
323
|
+
"无法安全补齐索引。请恢复唯一的本地文件或运行 pull 后重试。",
|
|
324
|
+
file=sys.stderr,
|
|
325
|
+
)
|
|
326
|
+
sys.exit(1)
|
|
327
|
+
meta[file_field] = candidates[0]
|
|
328
|
+
for field in drop_fields:
|
|
329
|
+
meta.pop(field, None)
|
|
330
|
+
items.append(meta)
|
|
331
|
+
recovered.append(value)
|
|
332
|
+
|
|
333
|
+
_writeback_index(rel_index, items)
|
|
334
|
+
print(f" OK {type_name}: 已从云端补齐 index.json 目标 {', '.join(recovered)},继续推送本地内容")
|
|
335
|
+
return items
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _orphan_hints(rel_dir, prefix, missing_ids, exts):
|
|
339
|
+
base = DEFAULT_ROOT / ".draftgo" / rel_dir
|
|
340
|
+
if not base.exists():
|
|
341
|
+
return []
|
|
342
|
+
hints = []
|
|
343
|
+
for mid in missing_ids:
|
|
344
|
+
for ext in exts:
|
|
345
|
+
for p in base.glob(f"{prefix}_{mid}_*{ext}"):
|
|
346
|
+
hints.append(f".draftgo/{rel_dir}/{p.name}")
|
|
347
|
+
return hints
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _warn_orphan_files(rel_dir, items, file_field, prefix, exts, type_name):
|
|
351
|
+
base = DEFAULT_ROOT / ".draftgo" / rel_dir
|
|
352
|
+
if not base.exists():
|
|
353
|
+
return
|
|
354
|
+
indexed = {str(it.get(file_field, "")).replace("\\", "/") for it in items}
|
|
355
|
+
orphans = []
|
|
356
|
+
for p in base.iterdir():
|
|
357
|
+
if not p.is_file():
|
|
358
|
+
continue
|
|
359
|
+
if not p.name.startswith(f"{prefix}_") or p.suffix not in exts:
|
|
360
|
+
continue
|
|
361
|
+
rel = f".draftgo/{rel_dir}/{p.name}"
|
|
362
|
+
if rel not in indexed:
|
|
363
|
+
orphans.append(rel)
|
|
364
|
+
if orphans:
|
|
365
|
+
print(f" WARN {type_name}: 发现 {len(orphans)} 个未登记到 index.json 的本地文件,本次不会推送:")
|
|
366
|
+
for rel in orphans[:8]:
|
|
367
|
+
print(f" - {rel}")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _summary(type_name, targets):
|
|
371
|
+
print(f" SUMMARY {type_name}: targets={targets}")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _preview(type_name, items, id_key="id"):
|
|
375
|
+
"""打印不会产生云端或本地副作用的推送预览。"""
|
|
376
|
+
if not DRY_RUN:
|
|
377
|
+
return False
|
|
378
|
+
for item in items:
|
|
379
|
+
identity = item.get(id_key)
|
|
380
|
+
label = item.get("title") or item.get("name") or item.get("code") or identity or "(new)"
|
|
381
|
+
action = "UPDATE" if identity else "CREATE"
|
|
382
|
+
print(f" DRY-RUN {action} [{label}]")
|
|
383
|
+
_summary(type_name, len(items))
|
|
384
|
+
return True
|
|
385
|
+
|
|
386
|
+
|
|
199
387
|
def sync_pages(server, token, ids=None):
|
|
200
|
-
all_pages = _load_index("pages/index.json")
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
pages
|
|
388
|
+
all_pages = _load_index("pages/index.json", allow_missing=bool(ids))
|
|
389
|
+
all_pages = _recover_missing_index_entries(
|
|
390
|
+
server, token, all_pages, ids, "id", "pages", "pages/index.json", "/api/pages/",
|
|
391
|
+
file_spec=("pages", "html_file", "page", [".html"]), drop_fields=("value",),
|
|
392
|
+
)
|
|
393
|
+
_warn_orphan_files("pages", all_pages, "html_file", "page", [".html"], "pages")
|
|
394
|
+
pages = _filter_items_by_ids(
|
|
395
|
+
all_pages, ids, "id", "pages",
|
|
396
|
+
orphan=lambda missing: _orphan_hints("pages", "page", missing, [".html"]),
|
|
397
|
+
)
|
|
398
|
+
if _preview("pages", pages):
|
|
399
|
+
return
|
|
204
400
|
dirty = False
|
|
205
401
|
for page in pages:
|
|
206
402
|
pid = page.get("id")
|
|
@@ -221,9 +417,9 @@ def sync_pages(server, token, ids=None):
|
|
|
221
417
|
"value": {"html": html},
|
|
222
418
|
}
|
|
223
419
|
if pid:
|
|
224
|
-
if not
|
|
420
|
+
if not _cloud_version_allows_push(server, token, f"/api/pages/{pid}", page, title):
|
|
225
421
|
continue
|
|
226
|
-
ok, status, body = api_call("PUT", server, token, f"/api/pages/{pid}", payload)
|
|
422
|
+
ok, status, body = api_call("PUT", server, token, f"/api/pages/{pid}", payload, acceptable_errors=(404,))
|
|
227
423
|
# PUT 404:本地 id 与云端不一致(删了重建 / 跨环境),按 route 自动创建
|
|
228
424
|
if not ok and status == 404:
|
|
229
425
|
print(f" WARN [{title}] page_id={pid} 不存在,尝试创建")
|
|
@@ -245,13 +441,17 @@ def sync_pages(server, token, ids=None):
|
|
|
245
441
|
print(f" ERR [{title}] 创建失败 -> {_info(ok, status, body)}")
|
|
246
442
|
if dirty:
|
|
247
443
|
_writeback_index("pages/index.json", all_pages)
|
|
444
|
+
_summary("pages", len(pages))
|
|
248
445
|
|
|
249
446
|
|
|
250
447
|
def sync_db_meta(server, token, ids=None):
|
|
251
|
-
all_metas = _load_index("db_meta/index.json")
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
448
|
+
all_metas = _load_index("db_meta/index.json", allow_missing=bool(ids))
|
|
449
|
+
all_metas = _recover_missing_index_entries(
|
|
450
|
+
server, token, all_metas, ids, "id", "db_meta", "db_meta/index.json", "/api/db-meta",
|
|
451
|
+
)
|
|
452
|
+
metas = _filter_items_by_ids(all_metas, ids, "id", "db_meta")
|
|
453
|
+
if _preview("db_meta", metas):
|
|
454
|
+
return
|
|
255
455
|
dirty = False
|
|
256
456
|
for meta in metas:
|
|
257
457
|
mid = meta.get("id")
|
|
@@ -266,9 +466,7 @@ def sync_db_meta(server, token, ids=None):
|
|
|
266
466
|
"extra": meta.get("extra"),
|
|
267
467
|
}
|
|
268
468
|
if mid:
|
|
269
|
-
|
|
270
|
-
continue
|
|
271
|
-
ok, status, body = api_call("PUT", server, token, f"/api/db-meta/{mid}", payload)
|
|
469
|
+
ok, status, body = api_call("PUT", server, token, f"/api/db-meta/{mid}", payload, acceptable_errors=(404,))
|
|
272
470
|
# 本地 index 的 id 与云端不一致(删了重建 / 跨环境同步)时 PUT 404,按 type 创建
|
|
273
471
|
if not ok and status == 404:
|
|
274
472
|
print(f" WARN [{label}] db_meta_id={mid} 不存在,尝试按 type 创建")
|
|
@@ -287,13 +485,22 @@ def sync_db_meta(server, token, ids=None):
|
|
|
287
485
|
print(f" ERR [{label}] 创建失败 -> {_info(ok, status, body)}")
|
|
288
486
|
if dirty:
|
|
289
487
|
_writeback_index("db_meta/index.json", all_metas)
|
|
488
|
+
_summary("db_meta", len(metas))
|
|
290
489
|
|
|
291
490
|
|
|
292
491
|
def sync_nav(server, token, ids=None):
|
|
293
|
-
all_navs = _load_index("navigations/index.json")
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
492
|
+
all_navs = _load_index("navigations/index.json", allow_missing=bool(ids))
|
|
493
|
+
all_navs = _recover_missing_index_entries(
|
|
494
|
+
server, token, all_navs, ids, "id", "nav", "navigations/index.json", "/api/navigations",
|
|
495
|
+
file_spec=("navigations", "html_file", "nav", [".html"]), drop_fields=("html",),
|
|
496
|
+
)
|
|
497
|
+
_warn_orphan_files("navigations", all_navs, "html_file", "nav", [".html"], "nav")
|
|
498
|
+
navs = _filter_items_by_ids(
|
|
499
|
+
all_navs, ids, "id", "nav",
|
|
500
|
+
orphan=lambda missing: _orphan_hints("navigations", "nav", missing, [".html"]),
|
|
501
|
+
)
|
|
502
|
+
if _preview("nav", navs):
|
|
503
|
+
return
|
|
297
504
|
dirty = False
|
|
298
505
|
for nav in navs:
|
|
299
506
|
nid = nav.get("id")
|
|
@@ -304,9 +511,7 @@ def sync_nav(server, token, ids=None):
|
|
|
304
511
|
continue
|
|
305
512
|
html = html_file.read_text(encoding="utf-8")
|
|
306
513
|
if nid:
|
|
307
|
-
|
|
308
|
-
continue
|
|
309
|
-
ok, status, body = api_call("PUT", server, token, f"/api/navigations/{nid}", {"html": html})
|
|
514
|
+
ok, status, body = api_call("PUT", server, token, f"/api/navigations/{nid}", {"html": html}, acceptable_errors=(404,))
|
|
310
515
|
if not ok and status == 404:
|
|
311
516
|
print(f" WARN [{name}] nav_id={nid} 不存在,尝试创建")
|
|
312
517
|
nid = None
|
|
@@ -336,13 +541,17 @@ def sync_nav(server, token, ids=None):
|
|
|
336
541
|
print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
|
|
337
542
|
if dirty:
|
|
338
543
|
_writeback_index("navigations/index.json", all_navs)
|
|
544
|
+
_summary("nav", len(navs))
|
|
339
545
|
|
|
340
546
|
|
|
341
547
|
def sync_aihub(server, token, ids=None):
|
|
342
|
-
all_items = _load_index("aihub/index.json")
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
548
|
+
all_items = _load_index("aihub/index.json", allow_missing=bool(ids))
|
|
549
|
+
all_items = _recover_missing_index_entries(
|
|
550
|
+
server, token, all_items, ids, "id", "aihub", "aihub/index.json", "/api/aihub",
|
|
551
|
+
)
|
|
552
|
+
items = _filter_items_by_ids(all_items, ids, "id", "aihub")
|
|
553
|
+
if _preview("aihub", items):
|
|
554
|
+
return
|
|
346
555
|
dirty = False
|
|
347
556
|
for it in items:
|
|
348
557
|
iid = it.get("id")
|
|
@@ -353,9 +562,7 @@ def sync_aihub(server, token, ids=None):
|
|
|
353
562
|
"tags", "describe", "permission", "status",
|
|
354
563
|
) if it.get(k) is not None}
|
|
355
564
|
if iid:
|
|
356
|
-
|
|
357
|
-
continue
|
|
358
|
-
ok, status, body = api_call("PUT", server, token, f"/api/aihub/{iid}", payload)
|
|
565
|
+
ok, status, body = api_call("PUT", server, token, f"/api/aihub/{iid}", payload, acceptable_errors=(404,))
|
|
359
566
|
if not ok and status == 404:
|
|
360
567
|
print(f" WARN [{name}] aihub_id={iid} 不存在,尝试创建")
|
|
361
568
|
iid = None
|
|
@@ -376,61 +583,25 @@ def sync_aihub(server, token, ids=None):
|
|
|
376
583
|
print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
|
|
377
584
|
if dirty:
|
|
378
585
|
_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)
|
|
586
|
+
_summary("aihub", len(items))
|
|
421
587
|
|
|
422
588
|
|
|
423
589
|
def sync_system_config(server, token, keys=None):
|
|
424
|
-
items = _load_index("system_config/index.json")
|
|
425
|
-
|
|
426
|
-
|
|
590
|
+
items = _load_index("system_config/index.json", allow_missing=bool(keys))
|
|
591
|
+
items = _recover_missing_index_entries(
|
|
592
|
+
server, token, items, keys, "config_key", "system_config", "system_config/index.json", "/api/system/config",
|
|
593
|
+
)
|
|
594
|
+
items = _filter_items_by_ids(items, keys, "config_key", "system_config")
|
|
595
|
+
if _preview("system_config", items, "config_key"):
|
|
596
|
+
return
|
|
427
597
|
for it in items:
|
|
428
598
|
ck = it.get("config_key")
|
|
429
599
|
if not ck:
|
|
430
600
|
continue
|
|
431
601
|
# 优先使用 parsed_value(init 时 GET 返回的解析值),其次 config_value
|
|
432
602
|
value = it.get("parsed_value") if "parsed_value" in it else it.get("config_value")
|
|
433
|
-
|
|
603
|
+
value_payload = {"config_value": value}
|
|
604
|
+
meta_payload = {
|
|
434
605
|
"config_value": value,
|
|
435
606
|
"value_type": it.get("value_type"),
|
|
436
607
|
"category": it.get("category"),
|
|
@@ -438,23 +609,66 @@ def sync_system_config(server, token, keys=None):
|
|
|
438
609
|
"is_sensitive": it.get("is_sensitive"),
|
|
439
610
|
"status": it.get("status"),
|
|
440
611
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
612
|
+
meta_payload = {k: v for k, v in meta_payload.items() if v is not None}
|
|
613
|
+
# 前端全局层属于系统默认配置,默认字段的描述/分类/状态由基座维护。
|
|
614
|
+
# 推送时只更新值,避免旧 index 中的元信息触发“系统默认字段不允许修改字段描述”。
|
|
615
|
+
payload = value_payload if _is_frontend_global_config(it) else meta_payload
|
|
616
|
+
ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", payload, acceptable_errors=(404,))
|
|
617
|
+
if not ok and _is_protected_system_config_error(body) and payload != value_payload:
|
|
618
|
+
print(f" WARN [{ck}] 系统默认字段元信息受保护,改为仅推送 config_value")
|
|
619
|
+
ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", value_payload, acceptable_errors=(404,))
|
|
445
620
|
if not ok and status == 404:
|
|
446
621
|
print(f" WARN [{ck}] system_config 不存在,尝试创建")
|
|
447
|
-
create_payload =
|
|
622
|
+
create_payload = _system_config_create_payload(ck, it, value)
|
|
448
623
|
ok, status, body = api_call("POST", server, token, "/api/system/", create_payload)
|
|
449
624
|
print(f" {'OK' if ok else 'ERR'} [{ck}] 已创建{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
450
625
|
else:
|
|
451
626
|
print(f" {'OK' if ok else 'ERR'} [{ck}]{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
627
|
+
_summary("system_config", len(items))
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _is_frontend_global_config(item):
|
|
631
|
+
return (
|
|
632
|
+
str(item.get("category") or "") == "frontend_global"
|
|
633
|
+
or str(item.get("config_key") or "").startswith("frontend_global_")
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _is_protected_system_config_error(body):
|
|
638
|
+
text = body if isinstance(body, str) else json.dumps(body, ensure_ascii=False)
|
|
639
|
+
return "系统默认字段不允许修改字段" in text
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _system_config_create_payload(config_key, item, value):
|
|
643
|
+
return {
|
|
644
|
+
"config_key": config_key,
|
|
645
|
+
"config_value": value,
|
|
646
|
+
"value_type": item.get("value_type") or _infer_system_config_value_type(value),
|
|
647
|
+
"category": item.get("category") or "custom",
|
|
648
|
+
"description": item.get("description") or "",
|
|
649
|
+
"is_sensitive": bool(item.get("is_sensitive", False)),
|
|
650
|
+
"status": item.get("status") or "active",
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _infer_system_config_value_type(value):
|
|
655
|
+
if isinstance(value, bool):
|
|
656
|
+
return "bool"
|
|
657
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
658
|
+
return "int"
|
|
659
|
+
if isinstance(value, (dict, list)):
|
|
660
|
+
return "json"
|
|
661
|
+
return "string"
|
|
452
662
|
|
|
453
663
|
|
|
454
664
|
def sync_roles(server, token, ids=None):
|
|
455
|
-
items = _load_index("roles/index.json")
|
|
456
|
-
|
|
457
|
-
|
|
665
|
+
items = _load_index("roles/index.json", allow_missing=bool(ids))
|
|
666
|
+
items = _recover_missing_index_entries(
|
|
667
|
+
server, token, items, ids, "id", "roles", "roles/index.json", "/api/roles",
|
|
668
|
+
)
|
|
669
|
+
items = _filter_items_by_ids(items, ids, "id", "roles")
|
|
670
|
+
if _preview("roles", items):
|
|
671
|
+
return
|
|
458
672
|
for it in items:
|
|
459
673
|
rid = it.get("id")
|
|
460
674
|
code = it.get("code", rid)
|
|
@@ -462,16 +676,19 @@ def sync_roles(server, token, ids=None):
|
|
|
462
676
|
payload = {k: it.get(k) for k in (
|
|
463
677
|
"name", "description", "status", "sort_order", "user_visible",
|
|
464
678
|
) 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
679
|
ok, status, body = api_call("PUT", server, token, f"/api/roles/{rid}", payload)
|
|
468
680
|
print(f" {'OK' if ok else 'ERR'} [{code}] role_id={rid}{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
681
|
+
_summary("roles", len(items))
|
|
469
682
|
|
|
470
683
|
|
|
471
684
|
def sync_users(server, token, ids=None):
|
|
472
|
-
items = _load_index("users/index.json")
|
|
473
|
-
|
|
474
|
-
|
|
685
|
+
items = _load_index("users/index.json", allow_missing=bool(ids))
|
|
686
|
+
items = _recover_missing_index_entries(
|
|
687
|
+
server, token, items, ids, "id", "users", "users/index.json", "/api/users",
|
|
688
|
+
)
|
|
689
|
+
items = _filter_items_by_ids(items, ids, "id", "users")
|
|
690
|
+
if _preview("users", items):
|
|
691
|
+
return
|
|
475
692
|
for it in items:
|
|
476
693
|
uid = it.get("id")
|
|
477
694
|
uname = it.get("username", uid)
|
|
@@ -480,18 +697,25 @@ def sync_users(server, token, ids=None):
|
|
|
480
697
|
"username", "email", "phone_number", "nickname",
|
|
481
698
|
"avatar", "status", "notes",
|
|
482
699
|
) 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
700
|
ok, status, body = api_call("PUT", server, token, f"/api/users/{uid}", payload)
|
|
486
701
|
print(f" {'OK' if ok else 'ERR'} [{uname}] user_id={uid}{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
702
|
+
_summary("users", len(items))
|
|
487
703
|
|
|
488
704
|
|
|
489
705
|
def sync_docs(server, token, ids=None):
|
|
490
706
|
"""文档文章:从 .draftgo/docs/articles/index.json + 同目录 .md 文件回填正文。"""
|
|
491
|
-
all_items = _load_index("docs/articles/index.json")
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
707
|
+
all_items = _load_index("docs/articles/index.json", allow_missing=bool(ids))
|
|
708
|
+
all_items = _recover_missing_index_entries(
|
|
709
|
+
server, token, all_items, ids, "id", "docs", "docs/articles/index.json", "/api/docs/admin/articles",
|
|
710
|
+
file_spec=("docs/articles", "content_file", "article", [".html", ".md"]), drop_fields=("content",),
|
|
711
|
+
)
|
|
712
|
+
_warn_orphan_files("docs/articles", all_items, "content_file", "article", [".html", ".md"], "docs")
|
|
713
|
+
items = _filter_items_by_ids(
|
|
714
|
+
all_items, ids, "id", "docs",
|
|
715
|
+
orphan=lambda missing: _orphan_hints("docs/articles", "article", missing, [".html", ".md"]),
|
|
716
|
+
)
|
|
717
|
+
if _preview("docs", items):
|
|
718
|
+
return
|
|
495
719
|
dirty = False
|
|
496
720
|
for it in items:
|
|
497
721
|
aid = it.get("id")
|
|
@@ -509,10 +733,9 @@ def sync_docs(server, token, ids=None):
|
|
|
509
733
|
"permission",
|
|
510
734
|
) if it.get(k) is not None}
|
|
511
735
|
payload["content"] = content
|
|
736
|
+
payload["content_type"] = "html"
|
|
512
737
|
if aid:
|
|
513
|
-
|
|
514
|
-
continue
|
|
515
|
-
ok, status, body = api_call("PUT", server, token, f"/api/docs/articles/{aid}", payload)
|
|
738
|
+
ok, status, body = api_call("PUT", server, token, f"/api/docs/articles/{aid}", payload, acceptable_errors=(404,))
|
|
516
739
|
if not ok and status == 404:
|
|
517
740
|
print(f" WARN [{title}] article_id={aid} 不存在,尝试创建")
|
|
518
741
|
aid = None
|
|
@@ -525,7 +748,7 @@ def sync_docs(server, token, ids=None):
|
|
|
525
748
|
it["id"] = new_id
|
|
526
749
|
new_rel = _rename_to_canonical(
|
|
527
750
|
"docs/articles", it.get("content_file", ""), "article", new_id,
|
|
528
|
-
it.get("slug") or it.get("title") or new_id, "
|
|
751
|
+
it.get("slug") or it.get("title") or new_id, "html")
|
|
529
752
|
it["content_file"] = new_rel
|
|
530
753
|
dirty = True
|
|
531
754
|
print(f" OK [{title}] 已创建 article_id={new_id}(已回写 index)")
|
|
@@ -533,13 +756,17 @@ def sync_docs(server, token, ids=None):
|
|
|
533
756
|
print(f" ERR [{title}] 创建失败 -> {_info(ok, status, body)}")
|
|
534
757
|
if dirty:
|
|
535
758
|
_writeback_index("docs/articles/index.json", all_items)
|
|
759
|
+
_summary("docs", len(items))
|
|
536
760
|
|
|
537
761
|
|
|
538
762
|
def sync_doc_categories(server, token, ids=None):
|
|
539
|
-
all_items = _load_index("doc_categories/index.json")
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
763
|
+
all_items = _load_index("doc_categories/index.json", allow_missing=bool(ids))
|
|
764
|
+
all_items = _recover_missing_index_entries(
|
|
765
|
+
server, token, all_items, ids, "id", "doc_categories", "doc_categories/index.json", "/api/docs/categories?flat=true",
|
|
766
|
+
)
|
|
767
|
+
items = _filter_items_by_ids(all_items, ids, "id", "doc_categories")
|
|
768
|
+
if _preview("doc_categories", items):
|
|
769
|
+
return
|
|
543
770
|
dirty = False
|
|
544
771
|
for it in items:
|
|
545
772
|
cid = it.get("id")
|
|
@@ -549,9 +776,7 @@ def sync_doc_categories(server, token, ids=None):
|
|
|
549
776
|
"name", "slug", "description", "icon", "parent_id", "sort_order", "status",
|
|
550
777
|
) if it.get(k) is not None}
|
|
551
778
|
if cid:
|
|
552
|
-
|
|
553
|
-
continue
|
|
554
|
-
ok, status, body = api_call("PUT", server, token, f"/api/docs/categories/{cid}", payload)
|
|
779
|
+
ok, status, body = api_call("PUT", server, token, f"/api/docs/categories/{cid}", payload, acceptable_errors=(404,))
|
|
555
780
|
if not ok and status == 404:
|
|
556
781
|
print(f" WARN [{name}] category_id={cid} 不存在,尝试创建")
|
|
557
782
|
cid = None
|
|
@@ -568,26 +793,45 @@ def sync_doc_categories(server, token, ids=None):
|
|
|
568
793
|
print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
|
|
569
794
|
if dirty:
|
|
570
795
|
_writeback_index("doc_categories/index.json", all_items)
|
|
796
|
+
_summary("doc_categories", len(items))
|
|
571
797
|
|
|
572
798
|
|
|
573
|
-
def
|
|
574
|
-
|
|
799
|
+
def _route_specs_from_code(code):
|
|
800
|
+
specs = []
|
|
801
|
+
for m in re.finditer(r'@route\(\s*["\']([A-Za-z]+)\s+([^"\']+)["\']\s*\)', code):
|
|
802
|
+
specs.append((m.group(1).upper(), m.group(2)))
|
|
803
|
+
for m in re.finditer(r'\.Route\(\s*["\']([A-Za-z]+)["\']\s*,\s*["\']([^"\']+)["\']', code):
|
|
804
|
+
specs.append((m.group(1).upper(), m.group(2)))
|
|
805
|
+
return specs
|
|
575
806
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
807
|
+
|
|
808
|
+
def _probe_script_routes(server, token, script_meta, code):
|
|
809
|
+
slug = script_meta.get("slug")
|
|
810
|
+
if not slug:
|
|
811
|
+
return
|
|
812
|
+
for method, route_path in _route_specs_from_code(code):
|
|
813
|
+
if method != "GET":
|
|
814
|
+
print(f" SKIP probe [{slug}] {method} {route_path}(只自动探测 GET,避免副作用)")
|
|
815
|
+
continue
|
|
816
|
+
path = route_path if route_path.startswith("/") else f"/{route_path}"
|
|
817
|
+
ok, status, body = api_call("GET", server, token, f"/api/x/{slug}{path}")
|
|
818
|
+
print(f" {'OK' if ok else 'ERR'} probe GET /api/x/{slug}{path}{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
583
819
|
|
|
584
820
|
|
|
585
821
|
def sync_custom_scripts(server, token, ids=None):
|
|
586
822
|
"""自定义脚本:从 .draftgo/custom_scripts/index.json + 同目录代码文件回填 code。"""
|
|
587
|
-
all_items = _load_index("custom_scripts/index.json")
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
823
|
+
all_items = _load_index("custom_scripts/index.json", allow_missing=bool(ids))
|
|
824
|
+
all_items = _recover_missing_index_entries(
|
|
825
|
+
server, token, all_items, ids, "id", "custom_scripts", "custom_scripts/index.json", "/api/scripts/",
|
|
826
|
+
file_spec=("custom_scripts", "code_file", "script", [".py", ".js", ".ts", ".sh", ".go", ".txt"]), drop_fields=("code",),
|
|
827
|
+
)
|
|
828
|
+
_warn_orphan_files("custom_scripts", all_items, "code_file", "script", [".go"], "custom_scripts")
|
|
829
|
+
items = _filter_items_by_ids(
|
|
830
|
+
all_items, ids, "id", "custom_scripts",
|
|
831
|
+
orphan=lambda missing: _orphan_hints("custom_scripts", "script", missing, [".go"]),
|
|
832
|
+
)
|
|
833
|
+
if _preview("custom_scripts", items):
|
|
834
|
+
return
|
|
591
835
|
dirty = False
|
|
592
836
|
for it in items:
|
|
593
837
|
sid = it.get("id")
|
|
@@ -598,33 +842,34 @@ def sync_custom_scripts(server, token, ids=None):
|
|
|
598
842
|
print(f" SKIP [{name}] code_file not found: {code_rel}")
|
|
599
843
|
continue
|
|
600
844
|
code = code_path.read_text(encoding="utf-8")
|
|
601
|
-
normalized_triggers = _normalize_script_triggers(it.get("mode"), it.get("triggers"))
|
|
602
845
|
if sid:
|
|
603
|
-
if not
|
|
846
|
+
if not _cloud_version_allows_push(server, token, f"/api/scripts/{sid}", it, name):
|
|
604
847
|
continue
|
|
605
848
|
# ScriptUpdate 接受字段(不含 slug/mode,避免误改启停/路由)
|
|
606
849
|
payload = {k: it.get(k) for k in (
|
|
607
|
-
"name", "description", "config", "permission",
|
|
850
|
+
"name", "description", "config", "permission", "go_mod", "go_sum",
|
|
608
851
|
) if it.get(k) is not None}
|
|
609
|
-
payload["triggers"] = normalized_triggers
|
|
610
852
|
payload["code"] = code
|
|
611
|
-
ok, status, body = api_call("PUT", server, token, f"/api/scripts/{sid}", payload)
|
|
853
|
+
ok, status, body = api_call("PUT", server, token, f"/api/scripts/{sid}", payload, acceptable_errors=(404,))
|
|
612
854
|
if not ok and status == 404:
|
|
613
855
|
print(f" WARN [{name}] script_id={sid} 不存在,尝试创建")
|
|
614
856
|
sid = None
|
|
615
857
|
else:
|
|
616
858
|
print(f" {'OK' if ok else 'ERR'} [{name}] script_id={sid}{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
859
|
+
if ok and PROBE_ROUTES and str(it.get("mode") or "").lower() in ("route", "mixed"):
|
|
860
|
+
_probe_script_routes(server, token, it, code)
|
|
617
861
|
if not sid:
|
|
618
|
-
# ScriptCreate 必填 name/slug/code/mode
|
|
862
|
+
# ScriptCreate 必填 name/slug/code/mode;触发来源统一以代码装饰器为准
|
|
619
863
|
payload = {
|
|
620
864
|
"name": it.get("name", ""),
|
|
621
865
|
"slug": it.get("slug", ""),
|
|
622
866
|
"code": code,
|
|
623
867
|
"mode": it.get("mode", "route"),
|
|
624
|
-
"triggers": normalized_triggers,
|
|
625
868
|
"config": it.get("config"),
|
|
626
869
|
"permission": it.get("permission"),
|
|
627
870
|
"description": it.get("description"),
|
|
871
|
+
"go_mod": it.get("go_mod"),
|
|
872
|
+
"go_sum": it.get("go_sum"),
|
|
628
873
|
}
|
|
629
874
|
payload = {k: v for k, v in payload.items() if v is not None}
|
|
630
875
|
ok, status, body = api_call("POST", server, token, "/api/scripts/", payload)
|
|
@@ -638,10 +883,13 @@ def sync_custom_scripts(server, token, ids=None):
|
|
|
638
883
|
it["code_file"] = new_rel
|
|
639
884
|
dirty = True
|
|
640
885
|
print(f" OK [{name}] 已创建 script_id={new_id}(已回写 index)")
|
|
886
|
+
if PROBE_ROUTES and str(it.get("mode") or "").lower() in ("route", "mixed"):
|
|
887
|
+
_probe_script_routes(server, token, it, code)
|
|
641
888
|
else:
|
|
642
889
|
print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
|
|
643
890
|
if dirty:
|
|
644
891
|
_writeback_index("custom_scripts/index.json", all_items)
|
|
892
|
+
_summary("custom_scripts", len(items))
|
|
645
893
|
|
|
646
894
|
|
|
647
895
|
def _lint_page_html(html, label):
|
|
@@ -693,7 +941,6 @@ HANDLERS = {
|
|
|
693
941
|
"nav": sync_nav,
|
|
694
942
|
"db_meta": sync_db_meta,
|
|
695
943
|
"aihub": sync_aihub,
|
|
696
|
-
"external_apis": sync_external_apis,
|
|
697
944
|
"system_config": sync_system_config,
|
|
698
945
|
"roles": sync_roles,
|
|
699
946
|
"users": sync_users,
|
|
@@ -723,32 +970,39 @@ def run_batch(server, token, args):
|
|
|
723
970
|
for mode, ids in groups:
|
|
724
971
|
print(f"\n--- {mode} {'(all)' if not ids else ','.join(ids)} ---")
|
|
725
972
|
HANDLERS[mode](server, token, ids)
|
|
726
|
-
|
|
973
|
+
result = "previewed" if DRY_RUN else "pushed"
|
|
974
|
+
print(f"\n[batch] done, {len(groups)} group(s) {result}")
|
|
727
975
|
|
|
728
976
|
|
|
729
977
|
def main():
|
|
730
|
-
global
|
|
731
|
-
|
|
978
|
+
global PROBE_ROUTES, DRY_RUN
|
|
732
979
|
if len(sys.argv) < 2:
|
|
733
980
|
print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]\n"
|
|
734
|
-
f" draftgo_push.py --batch <mode> <ids> [<mode> <ids> ...]
|
|
735
|
-
f" --force 跳过冲突检测,强制覆盖")
|
|
981
|
+
f" draftgo_push.py --batch <mode> <ids> [<mode> <ids> ...]")
|
|
736
982
|
sys.exit(1)
|
|
737
983
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
984
|
+
args = sys.argv[1:]
|
|
985
|
+
if "--probe-routes" in args:
|
|
986
|
+
PROBE_ROUTES = True
|
|
987
|
+
args = [a for a in args if a != "--probe-routes"]
|
|
988
|
+
if "--dry-run" in args:
|
|
989
|
+
DRY_RUN = True
|
|
990
|
+
args = [a for a in args if a != "--dry-run"]
|
|
743
991
|
|
|
744
992
|
if not args:
|
|
745
993
|
print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]")
|
|
746
994
|
sys.exit(1)
|
|
747
995
|
|
|
996
|
+
if args[0] == "--all":
|
|
997
|
+
args = ["--batch", *HANDLERS]
|
|
998
|
+
|
|
748
999
|
if args[0] == "--batch":
|
|
749
1000
|
server, token, cfg = load_config()
|
|
750
1001
|
run_batch(server, token, args[1:])
|
|
751
1002
|
_lessons_reminder(cfg)
|
|
1003
|
+
if RUN_FAILURES:
|
|
1004
|
+
print(f"ERR: push finished with {RUN_FAILURES} failed request(s)", file=sys.stderr)
|
|
1005
|
+
sys.exit(1)
|
|
752
1006
|
return
|
|
753
1007
|
|
|
754
1008
|
if args[0] not in HANDLERS:
|
|
@@ -759,6 +1013,9 @@ def main():
|
|
|
759
1013
|
server, token, cfg = load_config()
|
|
760
1014
|
HANDLERS[mode](server, token, ids)
|
|
761
1015
|
_lessons_reminder(cfg)
|
|
1016
|
+
if RUN_FAILURES:
|
|
1017
|
+
print(f"ERR: push finished with {RUN_FAILURES} failed request(s)", file=sys.stderr)
|
|
1018
|
+
sys.exit(1)
|
|
762
1019
|
|
|
763
1020
|
|
|
764
1021
|
if __name__ == "__main__":
|