draftgo-cli 2.0.9 → 3.0.1
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 +6 -0
- package/package.json +1 -1
- package/resources/skill/SKILL.md +74 -1378
- package/resources/skill/core/architecture.md +91 -0
- package/resources/skill/core/modules.md +54 -0
- package/resources/skill/practices/anti-patterns.md +70 -0
- package/resources/skill/practices/best-practices.md +41 -0
- package/resources/skill/practices/dev-declaration.md +94 -0
- package/resources/skill/push/SKILL.md +32 -1
- package/resources/skill/quickref/api-endpoints.md +130 -0
- package/resources/skill/quickref/api.json +17675 -0
- package/resources/skill/quickref/app-api.md +110 -0
- package/resources/skill/quickref/dg-components.md +198 -0
- package/resources/skill/rules/frontend.md +129 -596
- package/resources/skill/scripts/draftgo_delete.py +151 -0
- package/resources/skill/scripts/draftgo_push.py +80 -6
- package/resources/skill/specs/data.md +108 -0
- package/resources/skill/specs/runtime.md +98 -0
- package/resources/skill/specs/security.md +74 -0
- package/resources/skill/specs/ui-protocol.md +68 -0
- package/src/commands/delete.js +86 -0
- package/src/commands/help.js +7 -0
- package/src/commands/new.js +183 -0
- package/src/commands/sync.js +4 -1
- package/src/index.js +6 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
DraftGo Delete Script
|
|
4
|
+
删除云端资源,并同步移除本地 index.json 条目和文件。
|
|
5
|
+
|
|
6
|
+
用法:
|
|
7
|
+
python draftgo_delete.py pages <id>
|
|
8
|
+
python draftgo_delete.py nav <id>
|
|
9
|
+
python draftgo_delete.py db_meta <id>
|
|
10
|
+
python draftgo_delete.py custom_scripts <id>
|
|
11
|
+
python draftgo_delete.py docs <id>
|
|
12
|
+
python draftgo_delete.py doc_categories <id>
|
|
13
|
+
python draftgo_delete.py external_apis <id>
|
|
14
|
+
python draftgo_delete.py aihub <id>
|
|
15
|
+
"""
|
|
16
|
+
import json, sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import urllib.request, urllib.error
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def find_project_root(start: Path) -> Path:
|
|
24
|
+
cur = start
|
|
25
|
+
for _ in range(10):
|
|
26
|
+
if (cur / ".draftgo").is_dir():
|
|
27
|
+
return cur
|
|
28
|
+
if cur.parent == cur:
|
|
29
|
+
break
|
|
30
|
+
cur = cur.parent
|
|
31
|
+
cwd = Path.cwd()
|
|
32
|
+
if (cwd / ".draftgo").is_dir():
|
|
33
|
+
return cwd
|
|
34
|
+
return start.parents[3] if len(start.parents) >= 4 else start
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
DEFAULT_ROOT = find_project_root(SCRIPT_DIR)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_config():
|
|
41
|
+
cfg_path = DEFAULT_ROOT / ".draftgo/config.json"
|
|
42
|
+
if not cfg_path.exists():
|
|
43
|
+
print("ERR: .draftgo/config.json not found, run /draftgo init first", file=sys.stderr)
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
|
|
46
|
+
return cfg["server"].rstrip("/"), cfg["token"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def api_delete(server, token, path):
|
|
50
|
+
headers = {
|
|
51
|
+
"Authorization": f"Bearer {token}",
|
|
52
|
+
"Accept": "application/json",
|
|
53
|
+
}
|
|
54
|
+
req = urllib.request.Request(
|
|
55
|
+
f"{server}{path}",
|
|
56
|
+
method="DELETE",
|
|
57
|
+
headers=headers,
|
|
58
|
+
)
|
|
59
|
+
try:
|
|
60
|
+
with urllib.request.urlopen(req, timeout=15) as r:
|
|
61
|
+
return True, r.status, r.read().decode("utf-8", errors="replace")
|
|
62
|
+
except urllib.error.HTTPError as e:
|
|
63
|
+
return False, e.code, e.read().decode(errors="replace")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return False, None, str(e)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _load_index(rel_index):
|
|
69
|
+
new_path = DEFAULT_ROOT / ".draftgo" / rel_index
|
|
70
|
+
legacy_path = DEFAULT_ROOT / rel_index
|
|
71
|
+
path = new_path if new_path.exists() else legacy_path
|
|
72
|
+
if not path.exists():
|
|
73
|
+
print(f"ERR: {rel_index} not found", file=sys.stderr)
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
return path, json.loads(path.read_text(encoding="utf-8"))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _remove_local_file(rel_path):
|
|
79
|
+
if not rel_path:
|
|
80
|
+
return
|
|
81
|
+
full = DEFAULT_ROOT / rel_path
|
|
82
|
+
if full.exists():
|
|
83
|
+
full.unlink()
|
|
84
|
+
print(f" deleted local file: {rel_path}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# type → (api_prefix, index_rel, id_field, file_field)
|
|
88
|
+
TYPE_MAP = {
|
|
89
|
+
"pages": ("/api/pages", "pages/index.json", "id", "html_file"),
|
|
90
|
+
"nav": ("/api/navigations", "navigations/index.json", "id", "html_file"),
|
|
91
|
+
"db_meta": ("/api/db-meta", "db_meta/index.json", "id", None),
|
|
92
|
+
"custom_scripts": ("/api/scripts", "custom_scripts/index.json", "id", "code_file"),
|
|
93
|
+
"docs": ("/api/docs/articles", "docs/articles/index.json", "id", "content_file"),
|
|
94
|
+
"doc_categories": ("/api/docs/categories", "doc_categories/index.json", "id", None),
|
|
95
|
+
"external_apis": ("/api/external-apis", "external_apis/index.json", "id", None),
|
|
96
|
+
"aihub": ("/api/aihub", "aihub/index.json", "id", None),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def delete_resource(type_name, resource_id):
|
|
101
|
+
if type_name not in TYPE_MAP:
|
|
102
|
+
print(f"ERR: unknown type '{type_name}'. Supported: {', '.join(TYPE_MAP)}", file=sys.stderr)
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
|
|
105
|
+
api_prefix, index_rel, id_field, file_field = TYPE_MAP[type_name]
|
|
106
|
+
server, token = load_config()
|
|
107
|
+
|
|
108
|
+
# 1. 找到 index 条目
|
|
109
|
+
index_path, items = _load_index(index_rel)
|
|
110
|
+
item = next((it for it in items if str(it.get(id_field)) == str(resource_id)), None)
|
|
111
|
+
label = (
|
|
112
|
+
item.get("title") or item.get("name") or item.get("type") or item.get("slug") or resource_id
|
|
113
|
+
if item else resource_id
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# 2. 调用 DELETE API
|
|
117
|
+
ok, status, body = api_delete(server, token, f"{api_prefix}/{resource_id}")
|
|
118
|
+
if not ok and status != 404:
|
|
119
|
+
print(f" ERR [{label}] 删除失败: HTTP {status}: {body}")
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
if status == 404:
|
|
123
|
+
print(f" WARN [{label}] 云端不存在(id={resource_id}),仅清理本地")
|
|
124
|
+
else:
|
|
125
|
+
print(f" OK [{label}] 已从云端删除(id={resource_id})")
|
|
126
|
+
|
|
127
|
+
# 3. 删除本地文件
|
|
128
|
+
if item and file_field:
|
|
129
|
+
_remove_local_file(item.get(file_field))
|
|
130
|
+
|
|
131
|
+
# 4. 从 index 移除条目
|
|
132
|
+
new_items = [it for it in items if str(it.get(id_field)) != str(resource_id)]
|
|
133
|
+
if len(new_items) < len(items):
|
|
134
|
+
index_path.write_text(json.dumps(new_items, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
135
|
+
print(f" OK [{label}] 已从本地 index 移除")
|
|
136
|
+
else:
|
|
137
|
+
print(f" WARN [{label}] 本地 index 中未找到 id={resource_id}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main():
|
|
141
|
+
if len(sys.argv) < 3:
|
|
142
|
+
print(f"usage: draftgo_delete.py <{'|'.join(TYPE_MAP)}> <id>", file=sys.stderr)
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
type_name = sys.argv[1]
|
|
146
|
+
resource_id = sys.argv[2]
|
|
147
|
+
delete_resource(type_name, resource_id)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|
|
@@ -43,6 +43,45 @@ def find_project_root(start: Path) -> Path:
|
|
|
43
43
|
|
|
44
44
|
DEFAULT_ROOT = find_project_root(SCRIPT_DIR)
|
|
45
45
|
|
|
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
|
+
|
|
46
85
|
|
|
47
86
|
def load_config():
|
|
48
87
|
cfg_path = DEFAULT_ROOT / ".draftgo/config.json"
|
|
@@ -182,6 +221,8 @@ def sync_pages(server, token, ids=None):
|
|
|
182
221
|
"value": {"html": html},
|
|
183
222
|
}
|
|
184
223
|
if pid:
|
|
224
|
+
if not check_conflict(server, token, f"/api/pages/{pid}", page.get("updated_at"), title):
|
|
225
|
+
continue
|
|
185
226
|
ok, status, body = api_call("PUT", server, token, f"/api/pages/{pid}", payload)
|
|
186
227
|
# PUT 404:本地 id 与云端不一致(删了重建 / 跨环境),按 route 自动创建
|
|
187
228
|
if not ok and status == 404:
|
|
@@ -225,6 +266,8 @@ def sync_db_meta(server, token, ids=None):
|
|
|
225
266
|
"extra": meta.get("extra"),
|
|
226
267
|
}
|
|
227
268
|
if mid:
|
|
269
|
+
if not check_conflict(server, token, f"/api/db-meta/{mid}", meta.get("updated_at"), label):
|
|
270
|
+
continue
|
|
228
271
|
ok, status, body = api_call("PUT", server, token, f"/api/db-meta/{mid}", payload)
|
|
229
272
|
# 本地 index 的 id 与云端不一致(删了重建 / 跨环境同步)时 PUT 404,按 type 创建
|
|
230
273
|
if not ok and status == 404:
|
|
@@ -261,6 +304,8 @@ def sync_nav(server, token, ids=None):
|
|
|
261
304
|
continue
|
|
262
305
|
html = html_file.read_text(encoding="utf-8")
|
|
263
306
|
if nid:
|
|
307
|
+
if not check_conflict(server, token, f"/api/navigations/{nid}", nav.get("updated_at"), name):
|
|
308
|
+
continue
|
|
264
309
|
ok, status, body = api_call("PUT", server, token, f"/api/navigations/{nid}", {"html": html})
|
|
265
310
|
if not ok and status == 404:
|
|
266
311
|
print(f" WARN [{name}] nav_id={nid} 不存在,尝试创建")
|
|
@@ -308,6 +353,8 @@ def sync_aihub(server, token, ids=None):
|
|
|
308
353
|
"tags", "describe", "permission", "status",
|
|
309
354
|
) if it.get(k) is not None}
|
|
310
355
|
if iid:
|
|
356
|
+
if not check_conflict(server, token, f"/api/aihub/{iid}", it.get("updated_at"), name):
|
|
357
|
+
continue
|
|
311
358
|
ok, status, body = api_call("PUT", server, token, f"/api/aihub/{iid}", payload)
|
|
312
359
|
if not ok and status == 404:
|
|
313
360
|
print(f" WARN [{name}] aihub_id={iid} 不存在,尝试创建")
|
|
@@ -347,6 +394,8 @@ def sync_external_apis(server, token, ids=None):
|
|
|
347
394
|
"param_schema", "tags", "description", "status",
|
|
348
395
|
) if it.get(k) is not None}
|
|
349
396
|
if iid:
|
|
397
|
+
if not check_conflict(server, token, f"/api/external-apis/{iid}", it.get("updated_at"), code):
|
|
398
|
+
continue
|
|
350
399
|
ok, status, body = api_call("PUT", server, token, f"/api/external-apis/{iid}", payload)
|
|
351
400
|
if not ok and status == 404:
|
|
352
401
|
print(f" WARN [{code}] api_id={iid} 不存在,尝试创建")
|
|
@@ -390,6 +439,8 @@ def sync_system_config(server, token, keys=None):
|
|
|
390
439
|
"status": it.get("status"),
|
|
391
440
|
}
|
|
392
441
|
payload = {k: v for k, v in payload.items() if v is not None}
|
|
442
|
+
if not check_conflict(server, token, f"/api/system/{ck}", it.get("updated_at"), ck):
|
|
443
|
+
continue
|
|
393
444
|
ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", payload)
|
|
394
445
|
if not ok and status == 404:
|
|
395
446
|
print(f" WARN [{ck}] system_config 不存在,尝试创建")
|
|
@@ -411,6 +462,8 @@ def sync_roles(server, token, ids=None):
|
|
|
411
462
|
payload = {k: it.get(k) for k in (
|
|
412
463
|
"name", "description", "status", "sort_order", "user_visible",
|
|
413
464
|
) 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
|
|
414
467
|
ok, status, body = api_call("PUT", server, token, f"/api/roles/{rid}", payload)
|
|
415
468
|
print(f" {'OK' if ok else 'ERR'} [{code}] role_id={rid}{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
416
469
|
|
|
@@ -427,6 +480,8 @@ def sync_users(server, token, ids=None):
|
|
|
427
480
|
"username", "email", "phone_number", "nickname",
|
|
428
481
|
"avatar", "status", "notes",
|
|
429
482
|
) 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
|
|
430
485
|
ok, status, body = api_call("PUT", server, token, f"/api/users/{uid}", payload)
|
|
431
486
|
print(f" {'OK' if ok else 'ERR'} [{uname}] user_id={uid}{'' if ok else ' -> ' + _info(ok, status, body)}")
|
|
432
487
|
|
|
@@ -455,6 +510,8 @@ def sync_docs(server, token, ids=None):
|
|
|
455
510
|
) if it.get(k) is not None}
|
|
456
511
|
payload["content"] = content
|
|
457
512
|
if aid:
|
|
513
|
+
if not check_conflict(server, token, f"/api/docs/articles/{aid}", it.get("updated_at"), title):
|
|
514
|
+
continue
|
|
458
515
|
ok, status, body = api_call("PUT", server, token, f"/api/docs/articles/{aid}", payload)
|
|
459
516
|
if not ok and status == 404:
|
|
460
517
|
print(f" WARN [{title}] article_id={aid} 不存在,尝试创建")
|
|
@@ -492,6 +549,8 @@ def sync_doc_categories(server, token, ids=None):
|
|
|
492
549
|
"name", "slug", "description", "icon", "parent_id", "sort_order", "status",
|
|
493
550
|
) if it.get(k) is not None}
|
|
494
551
|
if cid:
|
|
552
|
+
if not check_conflict(server, token, f"/api/docs/categories/{cid}", it.get("updated_at"), name):
|
|
553
|
+
continue
|
|
495
554
|
ok, status, body = api_call("PUT", server, token, f"/api/docs/categories/{cid}", payload)
|
|
496
555
|
if not ok and status == 404:
|
|
497
556
|
print(f" WARN [{name}] category_id={cid} 不存在,尝试创建")
|
|
@@ -541,6 +600,8 @@ def sync_custom_scripts(server, token, ids=None):
|
|
|
541
600
|
code = code_path.read_text(encoding="utf-8")
|
|
542
601
|
normalized_triggers = _normalize_script_triggers(it.get("mode"), it.get("triggers"))
|
|
543
602
|
if sid:
|
|
603
|
+
if not check_conflict(server, token, f"/api/scripts/{sid}", it.get("updated_at"), name):
|
|
604
|
+
continue
|
|
544
605
|
# ScriptUpdate 接受字段(不含 slug/mode,避免误改启停/路由)
|
|
545
606
|
payload = {k: it.get(k) for k in (
|
|
546
607
|
"name", "description", "config", "permission",
|
|
@@ -666,22 +727,35 @@ def run_batch(server, token, args):
|
|
|
666
727
|
|
|
667
728
|
|
|
668
729
|
def main():
|
|
730
|
+
global FORCE
|
|
731
|
+
|
|
669
732
|
if len(sys.argv) < 2:
|
|
670
733
|
print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]\n"
|
|
671
|
-
f" draftgo_push.py --batch <mode> <ids> [<mode> <ids> ...]"
|
|
734
|
+
f" draftgo_push.py --batch <mode> <ids> [<mode> <ids> ...]\n"
|
|
735
|
+
f" --force 跳过冲突检测,强制覆盖")
|
|
736
|
+
sys.exit(1)
|
|
737
|
+
|
|
738
|
+
# 解析 --force(可出现在任意位置)
|
|
739
|
+
args = [a for a in sys.argv[1:] if a != "--force"]
|
|
740
|
+
if len(args) < len(sys.argv) - 1:
|
|
741
|
+
FORCE = True
|
|
742
|
+
print("[force] 已跳过冲突检测,将强制覆盖云端数据")
|
|
743
|
+
|
|
744
|
+
if not args:
|
|
745
|
+
print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]")
|
|
672
746
|
sys.exit(1)
|
|
673
747
|
|
|
674
|
-
if
|
|
748
|
+
if args[0] == "--batch":
|
|
675
749
|
server, token, cfg = load_config()
|
|
676
|
-
run_batch(server, token,
|
|
750
|
+
run_batch(server, token, args[1:])
|
|
677
751
|
_lessons_reminder(cfg)
|
|
678
752
|
return
|
|
679
753
|
|
|
680
|
-
if
|
|
754
|
+
if args[0] not in HANDLERS:
|
|
681
755
|
print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]")
|
|
682
756
|
sys.exit(1)
|
|
683
|
-
mode =
|
|
684
|
-
ids =
|
|
757
|
+
mode = args[0]
|
|
758
|
+
ids = args[1:] or None
|
|
685
759
|
server, token, cfg = load_config()
|
|
686
760
|
HANDLERS[mode](server, token, ids)
|
|
687
761
|
_lessons_reminder(cfg)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
read_when: 操作动态 DB 之前 · 设计数据结构时 · 使用 filters 检索时
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 动态 DB & 数据层
|
|
6
|
+
|
|
7
|
+
## DB Meta 结构
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"type": "order",
|
|
12
|
+
"label": "订单",
|
|
13
|
+
"schema": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": {
|
|
16
|
+
"name": { "type": "string", "title": "姓名", "required": true, "searchable": "fuzzy" },
|
|
17
|
+
"status": { "type": "string", "title": "状态", "required": false, "searchable": "exact" },
|
|
18
|
+
"amount": { "type": "number", "title": "金额", "required": false, "searchable": "range" },
|
|
19
|
+
"tags": { "type": "array", "title": "标签", "required": false, "searchable": "contains" },
|
|
20
|
+
"note": { "type": "string", "title": "备注", "required": false, "searchable": false }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"permission": {
|
|
24
|
+
"public": { "read": "none", "create": "none", "update": "none", "delete": "none" },
|
|
25
|
+
"login": { "read": "all", "create": "all", "update": "owner", "delete": "owner" },
|
|
26
|
+
"admin": { "read": "all", "create": "all", "update": "all", "delete": "all" }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**searchable 模式**:`false`(不可检索)/ `"exact"`(精确)/ `"fuzzy"`(模糊)/ `"range"`(数值范围)/ `"contains"`(数组包含)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## CRUD 操作范式
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
// 查询(分页 + 结构化检索)
|
|
39
|
+
const res = await App.get(`db/order`, {
|
|
40
|
+
page: 1, page_size: 20,
|
|
41
|
+
filters: ['name:like:张', 'status:eq:paid', 'amount:gte:100'],
|
|
42
|
+
order_by: 'amount', order: 'desc',
|
|
43
|
+
});
|
|
44
|
+
const { items, total } = res.data;
|
|
45
|
+
|
|
46
|
+
// 创建(业务字段必须放在 data 包裹里)
|
|
47
|
+
await App.post(`db/order`, { data: { name: '张三', amount: 200 }, status: 1 });
|
|
48
|
+
|
|
49
|
+
// 更新
|
|
50
|
+
await App.put(`db/order/${id}`, { data: { amount: 250 } });
|
|
51
|
+
|
|
52
|
+
// 删除
|
|
53
|
+
await App.delete(`db/order/${id}`);
|
|
54
|
+
|
|
55
|
+
// 批量更新(原子事务,任一失败全批回滚)
|
|
56
|
+
await App.patch(`db/order/batch`, [
|
|
57
|
+
{ id: 1, data: { status: 'paid' } },
|
|
58
|
+
{ id: 2, data: { status: 'paid' } },
|
|
59
|
+
]);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## filters 操作符
|
|
65
|
+
|
|
66
|
+
| 操作符 | 含义 | 适用 searchable 模式 |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `eq` | 精确等于 | exact / fuzzy / range |
|
|
69
|
+
| `like` | 模糊包含 | fuzzy |
|
|
70
|
+
| `gte` / `lte` / `gt` / `lt` | 数值范围 | range |
|
|
71
|
+
| `in` | 枚举命中(值逗号分隔:`status:in:paid,pending`) | exact / fuzzy |
|
|
72
|
+
| `contains` | 数组字段包含某值 | contains |
|
|
73
|
+
|
|
74
|
+
- 省略操作符(`filters: ['name:张三']`)默认 `like`
|
|
75
|
+
- 多个 filters 为 AND
|
|
76
|
+
- 字段未标 searchable 或操作符不匹配 → 后端返回 400
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## db_meta 本地文件
|
|
81
|
+
|
|
82
|
+
开发前先读 `.draftgo/db_meta/index.json` 了解可用 type 和字段:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
[
|
|
86
|
+
{
|
|
87
|
+
"id": 1,
|
|
88
|
+
"type": "order",
|
|
89
|
+
"label": "订单",
|
|
90
|
+
"schema": { ... }
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
⚠️ **GET `/api/db-meta/{type}` 用 type(如 `order`),不是 id。PUT/DELETE 才用 id。**
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 通用筛选参数(非动态 DB)
|
|
100
|
+
|
|
101
|
+
用于 users / roles / pages / navigations / feedback 等标准资源:
|
|
102
|
+
|
|
103
|
+
| 参数 | 说明 |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `page` / `page_size` | 分页(不传返回全量,超 10000 条后端拒绝) |
|
|
106
|
+
| `search` | 全文搜索(动态 DB 不用此参数) |
|
|
107
|
+
| `status` | 状态过滤 |
|
|
108
|
+
| `type` / `tag` | 类型/标签过滤 |
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
read_when: 需要理解运行时机制时 · 处理 token/路由/事件相关问题时
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 运行时机制(Runtime)
|
|
6
|
+
|
|
7
|
+
## 壳层启动流程
|
|
8
|
+
|
|
9
|
+
1. `reloadSystemConfig()` — 拉取 `/api/system/config`,填充 `state.config`
|
|
10
|
+
2. `validateToken()` — 验证 `localStorage.dg_access_token`,成功后设置 `currentUser`
|
|
11
|
+
3. `loadSetupStatus()` — 检查系统是否已初始化
|
|
12
|
+
4. `loadPage()` — 根据当前路由拉取页面 HTML,注入 iframe
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## iframe 注入机制
|
|
17
|
+
|
|
18
|
+
壳层通过 `decorateFrameHtml(html, routeContext, theme)` 处理数据库页面 HTML,注入:
|
|
19
|
+
|
|
20
|
+
- `window.__DG_ROUTE_CONTEXT__` — 当前路由上下文(含 `query` 参数),冻结对象
|
|
21
|
+
- `window.__DG_QUERY__` — `routeContext.query` 快捷方式
|
|
22
|
+
- `window.__DG_GET_ROUTE_CONTEXT__()` — 函数形式兜底读取
|
|
23
|
+
- 主题 CSS 变量(写入 `<head>` 最顶部,DOM 解析时即生效)
|
|
24
|
+
- 静态资源(Tailwind runtime、FontAwesome、GSAP 等)
|
|
25
|
+
|
|
26
|
+
页面以 `iframe.srcdoc` 渲染,**不是独立 URL**,因此:
|
|
27
|
+
- `window.location` 指向壳层地址,**不可用于读取路由参数**
|
|
28
|
+
- `window.parent.App` 是壳层暴露的能力对象
|
|
29
|
+
- `window.location.href = '/path'` 只跳 iframe 自身!导航必须用 `window.parent.location.href`
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## App 对象来源
|
|
34
|
+
|
|
35
|
+
壳层将 `app` 对象赋值给 `window.App`,页面通过 `window.parent.App` 访问。
|
|
36
|
+
|
|
37
|
+
`app` 组成:`api.js`(请求)+ `feedback.js`(弹窗/Toast)+ `runtime.js`(路由/主题)+ `i18n.js`(国际化)+ state(currentUser/isAdmin/config/theme)
|
|
38
|
+
|
|
39
|
+
完整 API 见 `{{SKILL_DIR}}/quickref/app-api.md`
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Token 存储
|
|
44
|
+
|
|
45
|
+
| key | 说明 |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `localStorage.dg_access_token` | 访问 token |
|
|
48
|
+
| `localStorage.dg_refresh_token` | 刷新 token |
|
|
49
|
+
|
|
50
|
+
登录后调用 `App.setAuthTokens({ access_token, refresh_token })` 写入并触发验证。
|
|
51
|
+
401 时壳层自动用 refresh token 刷新,无需页面处理。
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## URL 参数读取(标准三阶回落)
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
const routeContext =
|
|
59
|
+
window.__DG_ROUTE_CONTEXT__
|
|
60
|
+
|| window.__DG_GET_ROUTE_CONTEXT__?.()
|
|
61
|
+
|| window.parent?.App?.getCurrentRouteContext?.()
|
|
62
|
+
|| { query: {} };
|
|
63
|
+
const query = routeContext.query || {};
|
|
64
|
+
|
|
65
|
+
// 路由 /orders?orderId=42
|
|
66
|
+
const orderId = query.orderId; // "42"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**禁止**:`new URLSearchParams(window.location.search)` / 直接读 `window.location.search`
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 全局事件
|
|
74
|
+
|
|
75
|
+
| 事件名 | 触发时机 |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `dg:auth-ready` | token 验证完成(成功或失败) |
|
|
78
|
+
| `dg:auth-changed` | token 变更(登录/登出) |
|
|
79
|
+
|
|
80
|
+
监听方式:`window.parent.addEventListener('dg:auth-ready', handler)`
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 前端全局层
|
|
85
|
+
|
|
86
|
+
全局浮窗、客服入口、统计脚本等属于全局层,不属于单个业务页面。
|
|
87
|
+
|
|
88
|
+
存储:`sys_config.category = frontend_global`
|
|
89
|
+
|
|
90
|
+
| config_key | 用途 |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `frontend_global_head_html` | 注入壳层 head |
|
|
93
|
+
| `frontend_global_body_html` | 注入壳层 body 末尾 |
|
|
94
|
+
| `frontend_global_css` / `_js` | 壳层全局 CSS/JS |
|
|
95
|
+
| `frontend_global_iframe_head_html` | 注入每个业务页面 iframe head |
|
|
96
|
+
| `frontend_global_widget_html/css/js` | 全局挂件层 |
|
|
97
|
+
|
|
98
|
+
规则:**不能在业务页面内重复实现全局层能力;不允许新增槽位,只能编辑内置槽位。**
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
read_when: 开发前检查合规性时 · Code Review 时
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 开发禁区(违反必报错)
|
|
6
|
+
|
|
7
|
+
## 网络资源
|
|
8
|
+
|
|
9
|
+
| ❌ 禁止 | ✅ 替代 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| 任何境外 CDN(googleapis / jsdelivr / cdnjs / unpkg) | 国内镜像(npmmirror.com / staticfile.net)或本地资源 |
|
|
12
|
+
| 外部 CDN 引入图标 | `/assets/icons/{name}.svg` 内置图标库 |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## App 对象使用
|
|
17
|
+
|
|
18
|
+
| ❌ 禁止 | ✅ 替代 |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `App()` 写法(App 是对象不是函数) | `const App = window.parent?.App` |
|
|
21
|
+
| `const App = () => window.parent?.App` | 去掉箭头函数 |
|
|
22
|
+
| `App?.user?.role` 判断权限 | `App.isAdmin` 或 `App.currentUser?.role_code` |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 路由 & 导航
|
|
27
|
+
|
|
28
|
+
| ❌ 禁止 | ✅ 替代 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `window.location.search` 读参数 | `window.__DG_ROUTE_CONTEXT__.query` |
|
|
31
|
+
| `navigate('/login')` 退出 | `window.parent.location.href = '/login'` |
|
|
32
|
+
| 页面内 `window.location.href = ...` 跳转 | `window.parent.location.href = ...`(页面在 iframe 中) |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 弹窗 & 交互
|
|
37
|
+
|
|
38
|
+
| ❌ 禁止 | ✅ 替代 |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `window.alert(msg)` | `App.showModal(msg, title?)` |
|
|
41
|
+
| `window.confirm(msg)` | `await App.confirm(msg, title?)` |
|
|
42
|
+
| `window.prompt(msg)` | 自建 modal + input |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## UI 组件
|
|
47
|
+
|
|
48
|
+
| ❌ 禁止 | ✅ 替代 |
|
|
49
|
+
|---|---|
|
|
50
|
+
| 把 React/TSX 版 shadcn 组件写进数据库 HTML | 用对应 `dg-*` 标签 |
|
|
51
|
+
| 把 `dg-*` 当 daisyUI / Bootstrap / Ant Design | `dg-*` 只能是 shadcn 的 HTML 协议形态 |
|
|
52
|
+
| 硬编码 hex / rgb / rgba 颜色 | 用 `var(--dg-*)` 语义 token |
|
|
53
|
+
| 在页面内读 `localStorage.dg_theme` | `App.theme` |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 全局层
|
|
58
|
+
|
|
59
|
+
| ❌ 禁止 | ✅ 替代 |
|
|
60
|
+
|---|---|
|
|
61
|
+
| 在业务页面内实现全局浮窗 / 客服 / 统计脚本 | 使用 `frontend_global_*` 固定槽位 |
|
|
62
|
+
| 在页面内渲染系统 Header / Logo / 用户头像下拉 | 这些属于壳层,不属于业务页面 |
|
|
63
|
+
| 新增全局层槽位 | 只能编辑系统内置固定槽位 |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 颜色 Token 参考
|
|
68
|
+
|
|
69
|
+
| Token | 用途 |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `--dg-bg-base` / `--dg-bg-page` / `--dg-bg-surface` | 背景层级 |
|
|
72
|
+
| `--dg-text-primary` / `--dg-text-secondary` / `--dg-text-muted` | 文字层级 |
|
|
73
|
+
| `--dg-accent` / `--dg-accent-hover` / `--dg-accent-subtle` | 主题色 |
|
|
74
|
+
| `--dg-border` / `--dg-success` / `--dg-error` / `--dg-warning` | 功能色 |
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
read_when: 开发数据库页面时需要 dg-* 组件 · 了解 shadcn 映射关系时
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# dg-* 完整映射表
|
|
6
|
+
|
|
7
|
+
## 核心认知
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
dg-* ≠ 自研组件库
|
|
11
|
+
dg-* = shadcn/ui 在数据库 HTML 页面里的协议化表达
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
数据库页面不进入 Vite/React 编译链,不能写 TSX;改用 `dg-*` 标签,runtime 解析并渲染对应 shadcn 语义。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## shadcn → dg-* 完整映射
|
|
19
|
+
|
|
20
|
+
| shadcn 组件 | dg-* 标签 | 说明 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Button | `dg-button` | variant/size/disabled 属性对齐 |
|
|
23
|
+
| Card | `dg-card` + `dg-card-header/title/description/content/footer` | |
|
|
24
|
+
| Badge | `dg-badge` | variant: default/secondary/destructive/outline |
|
|
25
|
+
| Skeleton | `dg-skeleton` | 骨架屏 |
|
|
26
|
+
| Input | `dg-input` | type/placeholder/value/disabled |
|
|
27
|
+
| Textarea | `dg-textarea` | |
|
|
28
|
+
| Select | `dg-select` + `dg-select-item` | |
|
|
29
|
+
| Checkbox | `dg-checkbox` | |
|
|
30
|
+
| Switch | `dg-switch` | |
|
|
31
|
+
| RadioGroup | `dg-radio-group` + `dg-radio-item` | |
|
|
32
|
+
| Form | `dg-form` + `dg-form-item` + `dg-form-message` | |
|
|
33
|
+
| Table | `dg-table` | source 属性拉取数据,columns via data-dg-props |
|
|
34
|
+
| Tabs | `dg-tabs` + `dg-tabs-list/trigger/content` | default-value 属性 |
|
|
35
|
+
| Dialog | `dg-dialog` + `dg-dialog-trigger/content/header/title/footer` | |
|
|
36
|
+
| Sheet | `dg-sheet` + `dg-sheet-trigger/content/header/title` | side: right/left/top/bottom |
|
|
37
|
+
| DropdownMenu | `dg-dropdown-menu` + `dg-dropdown-menu-trigger/content/item/separator` | |
|
|
38
|
+
| Tooltip | `dg-tooltip` | content 属性 |
|
|
39
|
+
| Popover | `dg-popover` + `dg-popover-trigger/content` | |
|
|
40
|
+
| Alert | `dg-alert` + `dg-alert-title/description` | variant: default/destructive |
|
|
41
|
+
| Progress | `dg-progress` | value/max 属性 |
|
|
42
|
+
| Separator | `dg-separator` | orientation: horizontal/vertical |
|
|
43
|
+
| ScrollArea | `dg-scroll-area` | |
|
|
44
|
+
| Avatar | `dg-avatar` + `dg-avatar-image/fallback` | |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 属性约定
|
|
49
|
+
|
|
50
|
+
`dg-*` 属性命名跟随 shadcn 语义:
|
|
51
|
+
- `variant`、`size`、`disabled`、`default-value`、`data-state`、`aria-*` 保持原意
|
|
52
|
+
- 复杂组件使用 `data-dg-props='{"key":"value"}'` 传递结构化配置
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 禁止混淆
|
|
57
|
+
|
|
58
|
+
- ❌ `dg-*` 不是 daisyUI
|
|
59
|
+
- ❌ `dg-*` 不是 Bootstrap
|
|
60
|
+
- ❌ `dg-*` 不是 Ant Design / Element Plus
|
|
61
|
+
- ❌ `dg-*` 不是任意相似样式的泛称
|
|
62
|
+
- ✅ `dg-*` 只能是 shadcn 的 DraftGo HTML 协议形态
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 使用示例速查
|
|
67
|
+
|
|
68
|
+
完整带代码的示例见 `{{SKILL_DIR}}/quickref/dg-components.md`
|