@lark-apaas/coding-steering 0.1.32-beta.0 → 0.1.32-dev.3c06d32
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/package.json +6 -6
- package/steering/design-html/skills/charts/SKILL.md +4 -0
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +71 -26
- package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +26 -14
- package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +153 -8
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +2810 -292
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +544 -22
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +19 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +991 -165
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +22 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/query.py +4 -9
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +20 -12
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +556 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +364 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +76 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +528 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +1694 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +479 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +315 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +400 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
- package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
- package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +1 -0
- package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +6 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +5 -5
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +1 -1
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +37 -113
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +13 -2
- package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +7 -7
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +149 -24
- package/steering/nestjs-react-fullstack/skills/connections-sdk/SKILL.md +202 -0
- package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +255 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +158 -543
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +15 -1
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +30 -14
- package/steering/nestjs-react-fullstack/skills/raw-sql-boundary-audit/SKILL.md +63 -0
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +284 -12
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
- package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
- package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
- package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
- package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -269
- package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
- package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
- package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -311
- package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
- package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
- package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -257
- package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -104
- package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -199
- package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
- package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
- package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -165
- package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -91
- package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -165
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/references/trigger-lifecycle.md +0 -301
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify that generated slides honor every asset bound to their PPTX layout."""
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
import posixpath
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from collections import Counter
|
|
10
|
+
from html.parser import HTMLParser
|
|
11
|
+
from urllib.parse import urlsplit
|
|
12
|
+
|
|
13
|
+
from check_v2 import Pack
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
URL_RE = re.compile(r'url\(\s*[\'"]?([^\'")\s]+)', re.I)
|
|
17
|
+
VOID_TAGS = {
|
|
18
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
19
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def normalized_path(value):
|
|
24
|
+
path = urlsplit(value).path.replace('\\', '/')
|
|
25
|
+
return posixpath.normpath(path).lstrip('./')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def bound_asset_ids(value, known_asset_ids):
|
|
29
|
+
found = Counter()
|
|
30
|
+
if isinstance(value, dict):
|
|
31
|
+
for key, child in value.items():
|
|
32
|
+
if (key in ('asset', 'background')
|
|
33
|
+
and isinstance(child, str)
|
|
34
|
+
and child in known_asset_ids):
|
|
35
|
+
found[child] += 1
|
|
36
|
+
found.update(bound_asset_ids(child, known_asset_ids))
|
|
37
|
+
elif isinstance(value, list):
|
|
38
|
+
for child in value:
|
|
39
|
+
found.update(bound_asset_ids(child, known_asset_ids))
|
|
40
|
+
return found
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def layout_asset_contract(pack):
|
|
44
|
+
known_asset_ids = set(pack.assets)
|
|
45
|
+
owners = {}
|
|
46
|
+
for layout_name, (layout, _) in pack.layouts.items():
|
|
47
|
+
for asset_id, _, _ in layout_asset_instances(
|
|
48
|
+
layout, known_asset_ids, pack.canvas):
|
|
49
|
+
owners.setdefault(asset_id, set()).add(layout_name)
|
|
50
|
+
return owners
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def asset_urls(pack, asset_prefix, asset_ids):
|
|
54
|
+
prefix = normalized_path(asset_prefix).rstrip('/')
|
|
55
|
+
urls = {}
|
|
56
|
+
for asset_id in asset_ids:
|
|
57
|
+
entry, _ = pack.assets.get(asset_id, (None, None))
|
|
58
|
+
if not isinstance(entry, dict):
|
|
59
|
+
continue
|
|
60
|
+
path = entry.get('path')
|
|
61
|
+
if not isinstance(path, str) or not path:
|
|
62
|
+
continue
|
|
63
|
+
relative = normalized_path(path)
|
|
64
|
+
if relative.startswith('assets/'):
|
|
65
|
+
relative = relative[len('assets/'):]
|
|
66
|
+
urls.setdefault(posixpath.join(prefix, relative), set()).add(asset_id)
|
|
67
|
+
return urls
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def bound_asset_instances(value, known_asset_ids):
|
|
71
|
+
"""Return every positioned fixed-asset instance nested in slots or flow."""
|
|
72
|
+
instances = []
|
|
73
|
+
if isinstance(value, dict):
|
|
74
|
+
box = value.get('box')
|
|
75
|
+
asset_id = value.get('asset')
|
|
76
|
+
if (isinstance(box, list) and len(box) == 4
|
|
77
|
+
and isinstance(asset_id, str)
|
|
78
|
+
and asset_id in known_asset_ids):
|
|
79
|
+
instances.append((asset_id, value.get('role') or 'asset', box))
|
|
80
|
+
for key, child in value.items():
|
|
81
|
+
if key not in ('asset', 'background'):
|
|
82
|
+
instances.extend(bound_asset_instances(child, known_asset_ids))
|
|
83
|
+
elif isinstance(value, list):
|
|
84
|
+
for child in value:
|
|
85
|
+
instances.extend(bound_asset_instances(child, known_asset_ids))
|
|
86
|
+
return instances
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def layout_asset_instances(layout, known_asset_ids, canvas):
|
|
90
|
+
"""Return every fixed image instance declared by one layout."""
|
|
91
|
+
if not isinstance(layout, dict):
|
|
92
|
+
return []
|
|
93
|
+
instances = []
|
|
94
|
+
background = layout.get('background')
|
|
95
|
+
if canvas and isinstance(background, str) and background in known_asset_ids:
|
|
96
|
+
instances.append(
|
|
97
|
+
(background, 'background', [0, 0, canvas[0], canvas[1]]))
|
|
98
|
+
instances.extend(bound_asset_instances(layout, known_asset_ids))
|
|
99
|
+
return instances
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def inline_styles(value):
|
|
103
|
+
styles = {}
|
|
104
|
+
for declaration in (value or '').split(';'):
|
|
105
|
+
if ':' not in declaration:
|
|
106
|
+
continue
|
|
107
|
+
name, raw = declaration.split(':', 1)
|
|
108
|
+
styles[name.strip().lower()] = re.sub(
|
|
109
|
+
r'\s*!important\s*$', '', raw.strip().lower())
|
|
110
|
+
return styles
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def css_number(value):
|
|
114
|
+
match = re.fullmatch(r'(-?(?:\d+(?:\.\d*)?|\.\d+))(?:px)?', value or '')
|
|
115
|
+
return float(match.group(1)) if match else None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def element_box(reference, canvas):
|
|
119
|
+
styles = reference['styles']
|
|
120
|
+
if reference['slide_root'] and not any(
|
|
121
|
+
name in styles for name in ('left', 'top', 'width', 'height')):
|
|
122
|
+
return [0.0, 0.0, float(canvas[0]), float(canvas[1])]
|
|
123
|
+
values = [
|
|
124
|
+
css_number(styles.get(name))
|
|
125
|
+
for name in ('left', 'top', 'width', 'height')
|
|
126
|
+
]
|
|
127
|
+
if styles.get('position') != 'absolute' or any(
|
|
128
|
+
value is None for value in values):
|
|
129
|
+
return None
|
|
130
|
+
return values
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def boxes_match(actual, expected, tolerance=1.0):
|
|
134
|
+
return actual is not None and all(
|
|
135
|
+
abs(actual_value - expected_value) <= tolerance
|
|
136
|
+
for actual_value, expected_value in zip(actual, expected)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def file_sha256(path):
|
|
141
|
+
digest = hashlib.sha256()
|
|
142
|
+
with open(path, 'rb') as stream:
|
|
143
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
|
|
144
|
+
digest.update(chunk)
|
|
145
|
+
return digest.hexdigest()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def copied_asset_problems(pack, html_path, asset_prefix, asset_ids):
|
|
149
|
+
"""Verify copied fixed assets still contain the source PPTX bytes."""
|
|
150
|
+
problems = []
|
|
151
|
+
html_root = os.path.dirname(os.path.abspath(html_path))
|
|
152
|
+
prefix = normalized_path(asset_prefix).rstrip('/')
|
|
153
|
+
for asset_id in sorted(asset_ids):
|
|
154
|
+
entry, _ = pack.assets.get(asset_id, (None, None))
|
|
155
|
+
path = entry.get('path') if isinstance(entry, dict) else None
|
|
156
|
+
if not isinstance(path, str) or not path:
|
|
157
|
+
continue
|
|
158
|
+
relative = normalized_path(path)
|
|
159
|
+
if relative.startswith('assets/'):
|
|
160
|
+
relative = relative[len('assets/'):]
|
|
161
|
+
source_path = os.path.join(pack.root, 'assets', *relative.split('/'))
|
|
162
|
+
copied_relative = posixpath.join(prefix, relative)
|
|
163
|
+
copied_path = os.path.join(html_root, *copied_relative.split('/'))
|
|
164
|
+
if not os.path.isfile(copied_path):
|
|
165
|
+
problems.append(
|
|
166
|
+
'固定素材 %s 未复制到项目: %s' % (asset_id, copied_relative))
|
|
167
|
+
continue
|
|
168
|
+
if not os.path.isfile(source_path):
|
|
169
|
+
problems.append(
|
|
170
|
+
'风格包中的固定素材 %s 不存在: %s' % (asset_id, path))
|
|
171
|
+
continue
|
|
172
|
+
if file_sha256(copied_path) != file_sha256(source_path):
|
|
173
|
+
problems.append(
|
|
174
|
+
'固定素材 %s 已被替换或改写,必须使用 PPTX 原文件' % asset_id)
|
|
175
|
+
return problems
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def urls_from_attrs(attrs):
|
|
179
|
+
urls = []
|
|
180
|
+
for key, value in attrs:
|
|
181
|
+
if not value:
|
|
182
|
+
continue
|
|
183
|
+
if key.lower() in ('src', 'href'):
|
|
184
|
+
urls.append(value)
|
|
185
|
+
elif key.lower() == 'srcset':
|
|
186
|
+
urls.extend(item.strip().split(' ', 1)[0] for item in value.split(','))
|
|
187
|
+
elif key.lower() == 'style':
|
|
188
|
+
urls.extend(URL_RE.findall(value))
|
|
189
|
+
return urls
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SlideAssetParser(HTMLParser):
|
|
193
|
+
def __init__(self):
|
|
194
|
+
super().__init__()
|
|
195
|
+
self.slides = []
|
|
196
|
+
self._stack = []
|
|
197
|
+
self._style_depth = 0
|
|
198
|
+
self.outside_urls = []
|
|
199
|
+
|
|
200
|
+
def handle_starttag(self, tag, attrs):
|
|
201
|
+
tag = tag.lower()
|
|
202
|
+
attrs_map = {key.lower(): value for key, value in attrs}
|
|
203
|
+
urls = urls_from_attrs(attrs)
|
|
204
|
+
parent = self._stack[-1] if self._stack else None
|
|
205
|
+
parent_slide = parent['slide'] if parent else None
|
|
206
|
+
slide_root = bool(
|
|
207
|
+
tag == 'section' and parent and parent['tag'] == 'deck-stage')
|
|
208
|
+
slide = ({
|
|
209
|
+
'layout': attrs_map.get('data-pptx-layout'),
|
|
210
|
+
'references': [],
|
|
211
|
+
} if slide_root else parent_slide)
|
|
212
|
+
if slide_root:
|
|
213
|
+
self.slides.append(slide)
|
|
214
|
+
styles = inline_styles(attrs_map.get('style'))
|
|
215
|
+
hidden = bool(
|
|
216
|
+
(parent and parent['hidden'])
|
|
217
|
+
or 'hidden' in attrs_map
|
|
218
|
+
or attrs_map.get('aria-hidden', '').lower() == 'true'
|
|
219
|
+
or styles.get('display') == 'none'
|
|
220
|
+
or styles.get('visibility') in ('hidden', 'collapse')
|
|
221
|
+
or styles.get('content-visibility') == 'hidden'
|
|
222
|
+
or css_number(styles.get('width')) == 0
|
|
223
|
+
or css_number(styles.get('height')) == 0
|
|
224
|
+
or (
|
|
225
|
+
css_number(styles.get('opacity')) is not None
|
|
226
|
+
and css_number(styles.get('opacity')) <= 0
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
if slide is not None and (urls or attrs_map.get('data-pptx-asset')):
|
|
230
|
+
slide['references'].append({
|
|
231
|
+
'asset': attrs_map.get('data-pptx-asset'),
|
|
232
|
+
'hidden': hidden,
|
|
233
|
+
'slide_root': slide_root,
|
|
234
|
+
'source': attrs_map.get('src'),
|
|
235
|
+
'styles': styles,
|
|
236
|
+
'tag': tag,
|
|
237
|
+
'urls': urls,
|
|
238
|
+
})
|
|
239
|
+
elif urls:
|
|
240
|
+
self.outside_urls.extend(urls)
|
|
241
|
+
if tag == 'style':
|
|
242
|
+
self._style_depth += 1
|
|
243
|
+
if tag not in VOID_TAGS:
|
|
244
|
+
self._stack.append({
|
|
245
|
+
'hidden': hidden,
|
|
246
|
+
'slide': slide,
|
|
247
|
+
'tag': tag,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
def handle_startendtag(self, tag, attrs):
|
|
251
|
+
self.handle_starttag(tag, attrs)
|
|
252
|
+
if tag.lower() not in VOID_TAGS:
|
|
253
|
+
self.handle_endtag(tag)
|
|
254
|
+
|
|
255
|
+
def handle_endtag(self, tag):
|
|
256
|
+
tag = tag.lower()
|
|
257
|
+
if tag == 'style' and self._style_depth:
|
|
258
|
+
self._style_depth -= 1
|
|
259
|
+
if tag not in VOID_TAGS and self._stack:
|
|
260
|
+
self._stack.pop()
|
|
261
|
+
|
|
262
|
+
def handle_data(self, data):
|
|
263
|
+
if not self._style_depth:
|
|
264
|
+
return
|
|
265
|
+
self.outside_urls.extend(URL_RE.findall(data))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def validate_layout_assets(pack_dir, html_path, asset_prefix):
|
|
269
|
+
"""Return violations of the asset contract declared by each layout."""
|
|
270
|
+
pack = Pack(pack_dir)
|
|
271
|
+
owners = layout_asset_contract(pack)
|
|
272
|
+
known_urls = asset_urls(pack, asset_prefix, owners)
|
|
273
|
+
if not known_urls:
|
|
274
|
+
return []
|
|
275
|
+
asset_urls_by_id = {
|
|
276
|
+
asset_id: url
|
|
277
|
+
for url, asset_ids in known_urls.items()
|
|
278
|
+
for asset_id in asset_ids
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
with open(html_path, encoding='utf-8') as stream:
|
|
282
|
+
text = stream.read()
|
|
283
|
+
parser = SlideAssetParser()
|
|
284
|
+
parser.feed(text)
|
|
285
|
+
parser.close()
|
|
286
|
+
|
|
287
|
+
problems = copied_asset_problems(pack, html_path, asset_prefix, owners)
|
|
288
|
+
for url in parser.outside_urls:
|
|
289
|
+
for asset_id in sorted(known_urls.get(normalized_path(url), ())):
|
|
290
|
+
problems.append(
|
|
291
|
+
'模板资产 %s 出现在 slide section 外,无法核验页型归属' % asset_id)
|
|
292
|
+
for number, slide in enumerate(parser.slides, 1):
|
|
293
|
+
layout = slide['layout']
|
|
294
|
+
if not layout:
|
|
295
|
+
problems.append('第 %d 页缺少 data-pptx-layout,无法核验模板资产归属' % number)
|
|
296
|
+
continue
|
|
297
|
+
if layout not in pack.layouts:
|
|
298
|
+
problems.append('第 %d 页声明了不存在的模板页型: %s' % (number, layout))
|
|
299
|
+
continue
|
|
300
|
+
expected = layout_asset_instances(
|
|
301
|
+
pack.layouts[layout][0], set(pack.assets), pack.canvas)
|
|
302
|
+
actual = []
|
|
303
|
+
for reference in slide['references']:
|
|
304
|
+
referenced_ids = set()
|
|
305
|
+
normalized_urls = [normalized_path(url) for url in reference['urls']]
|
|
306
|
+
for url in normalized_urls:
|
|
307
|
+
referenced_ids.update(known_urls.get(url, ()))
|
|
308
|
+
asset_id = reference['asset']
|
|
309
|
+
if not asset_id:
|
|
310
|
+
for referenced_id in sorted(referenced_ids):
|
|
311
|
+
problems.append(
|
|
312
|
+
'第 %d 页模板资产 %s 缺少 data-pptx-asset 实例标记'
|
|
313
|
+
% (number, referenced_id))
|
|
314
|
+
continue
|
|
315
|
+
if asset_id not in pack.assets:
|
|
316
|
+
problems.append(
|
|
317
|
+
'第 %d 页声明了不存在的模板资产: %s' % (number, asset_id))
|
|
318
|
+
continue
|
|
319
|
+
expected_url = asset_urls_by_id.get(asset_id)
|
|
320
|
+
uses_expected_source = (
|
|
321
|
+
len(normalized_urls) == 1
|
|
322
|
+
and normalized_urls[0] == expected_url
|
|
323
|
+
and (
|
|
324
|
+
reference['tag'] != 'img'
|
|
325
|
+
or (
|
|
326
|
+
reference['source']
|
|
327
|
+
and normalized_path(reference['source']) == expected_url
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
if not uses_expected_source:
|
|
332
|
+
problems.append(
|
|
333
|
+
'第 %d 页固定实例 %s 未引用对应的 PPTX 原素材'
|
|
334
|
+
% (number, asset_id))
|
|
335
|
+
if reference['hidden']:
|
|
336
|
+
problems.append(
|
|
337
|
+
'第 %d 页固定实例 %s 不可隐藏' % (number, asset_id))
|
|
338
|
+
actual.append({
|
|
339
|
+
'asset': asset_id,
|
|
340
|
+
'box': element_box(reference, pack.canvas),
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
used_asset_counts = Counter(instance['asset'] for instance in actual)
|
|
344
|
+
for asset_id in sorted(used_asset_counts):
|
|
345
|
+
if layout not in owners.get(asset_id, set()):
|
|
346
|
+
allowed = '、'.join(sorted(owners.get(asset_id) or ())) or '(无)'
|
|
347
|
+
problems.append(
|
|
348
|
+
'第 %d 页页型 %s 不得使用 %s;只允许: %s'
|
|
349
|
+
% (number, layout, asset_id, allowed))
|
|
350
|
+
unmatched = list(actual)
|
|
351
|
+
for asset_id, _, expected_box in expected:
|
|
352
|
+
matching_index = next((
|
|
353
|
+
index for index, instance in enumerate(unmatched)
|
|
354
|
+
if instance['asset'] == asset_id
|
|
355
|
+
and boxes_match(instance['box'], expected_box)
|
|
356
|
+
), None)
|
|
357
|
+
if matching_index is not None:
|
|
358
|
+
unmatched.pop(matching_index)
|
|
359
|
+
continue
|
|
360
|
+
same_asset = next((
|
|
361
|
+
instance for instance in unmatched
|
|
362
|
+
if instance['asset'] == asset_id
|
|
363
|
+
), None)
|
|
364
|
+
if same_asset:
|
|
365
|
+
problems.append(
|
|
366
|
+
'第 %d 页固定实例 %s 的位置尺寸必须为 %s,当前为 %s'
|
|
367
|
+
% (number, asset_id, expected_box, same_asset['box']))
|
|
368
|
+
unmatched.remove(same_asset)
|
|
369
|
+
else:
|
|
370
|
+
problems.append(
|
|
371
|
+
'第 %d 页页型 %s 缺少固定实例 %s,位置尺寸应为 %s'
|
|
372
|
+
% (number, layout, asset_id, expected_box))
|
|
373
|
+
for instance in unmatched:
|
|
374
|
+
if layout in owners.get(instance['asset'], set()):
|
|
375
|
+
problems.append(
|
|
376
|
+
'第 %d 页页型 %s 额外使用了固定实例 %s'
|
|
377
|
+
% (number, layout, instance['asset']))
|
|
378
|
+
return problems
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def main(argv=None):
|
|
382
|
+
parser = argparse.ArgumentParser(
|
|
383
|
+
description='Verify that generated deck HTML honors PPTX layout asset bindings.')
|
|
384
|
+
parser.add_argument('pack_dir')
|
|
385
|
+
parser.add_argument('html_path')
|
|
386
|
+
parser.add_argument('--asset-prefix', required=True)
|
|
387
|
+
args = parser.parse_args(argv)
|
|
388
|
+
|
|
389
|
+
problems = validate_layout_assets(args.pack_dir, args.html_path, args.asset_prefix)
|
|
390
|
+
if not problems:
|
|
391
|
+
print('PPTX_LAYOUT_ASSETS: PASS')
|
|
392
|
+
return 0
|
|
393
|
+
print('PPTX_LAYOUT_ASSETS: FAIL count=%d' % len(problems))
|
|
394
|
+
for problem in problems:
|
|
395
|
+
print('[layoutAssets] %s' % problem)
|
|
396
|
+
return 1
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
if __name__ == '__main__':
|
|
400
|
+
sys.exit(main())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Backward-compatible entrypoint for PPTX layout asset verification."""
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from verify_layout_assets import main, validate_layout_assets
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
validate_logo_scope = validate_layout_assets
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if __name__ == '__main__':
|
|
12
|
+
sys.exit(main())
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
- 消费方直接读 design.md,靠目录约定找 sidecar 与资产;frontmatter `path` 是唯一文件引用点。
|
|
18
18
|
- manifest(`manifest.json`,识别靠内部 `schemaVersion` 字面量不靠文件名)服务存储、索引、校验和迁移;消费模型不需要读它。
|
|
19
|
-
- `ref/`
|
|
19
|
+
- `ref/` 只放 `audit.yaml`(数值出处的人工复核记录,几 KB)。频次原表、聚类原始数据、`extract.json`、重建图、logo 候选图**留在抽取工作目录,不进交付包**——原设计是「下发时链路剥离」,但链路上没有环节真的做剥离,审计材料会连带进消费上下文并占掉包体的大头。
|
|
20
20
|
|
|
21
21
|
## 1. design.md frontmatter 新增键
|
|
22
22
|
|
|
@@ -53,10 +53,10 @@ canvas: 1920x1080 # layouts.md 首键;px = round(EMU / sldSz_cx * 1920)
|
|
|
53
53
|
themes: [dark, light]
|
|
54
54
|
default-theme: dark # 双主题包必填(V2-13)
|
|
55
55
|
colors: # v1 单层扁平,主题进 token 名
|
|
56
|
-
dark-surface: "#
|
|
57
|
-
dark-on-surface: "#
|
|
58
|
-
light-surface: "#
|
|
59
|
-
primary: "#
|
|
56
|
+
dark-surface: "#RRGGBB"
|
|
57
|
+
dark-on-surface: "#RRGGBB"
|
|
58
|
+
light-surface: "#RRGGBB"
|
|
59
|
+
primary: "#RRGGBB" # 共用色不加前缀
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
- token 前缀约定(`dark-X`/`light-X` = 主题专属,无前缀 = 共用)在 `## Usage` 里向消费者写一句;主题机制溯源(clrMap 反转等)落 `ref/audit.yaml`。
|
|
@@ -65,7 +65,7 @@ colors: # v1 单层扁平,主题进 token 名
|
|
|
65
65
|
|
|
66
66
|
### 1.3 排除色
|
|
67
67
|
|
|
68
|
-
- **排除色进 `## Hard Rules`
|
|
68
|
+
- **排除色进 `## Hard Rules` 带证据计数**,正向给替代(如「`<hex>` 为编辑器参考线色(出现 <N> 次),非设计色」)。频次原表与 color-confidence 证据进 `ref/`。
|
|
69
69
|
- 排除色断言必须以**解析后频次**为准,`styleRef` 主题兜底引用(不渲染)与真实设计用色分开。
|
|
70
70
|
|
|
71
71
|
## 2. `assets` 段
|
|
@@ -84,7 +84,7 @@ assets:
|
|
|
84
84
|
kind: background
|
|
85
85
|
role: cover # cover | content | section | closing | accent
|
|
86
86
|
theme: dark
|
|
87
|
-
recipe: "linear-gradient(
|
|
87
|
+
recipe: "linear-gradient(<angle>, <color> 0%, <color> 100%)" # 可选:CSS 重绘配方
|
|
88
88
|
bg-content-solid: # 纯色背景:无 path/url,引 colors token
|
|
89
89
|
kind: background
|
|
90
90
|
role: content
|
|
@@ -105,11 +105,12 @@ assets:
|
|
|
105
105
|
|
|
106
106
|
design.md 是消费模型的操作文档,不是抽取记录。`## Usage` 承载三件事,全部**可执行**(具体文件、具体坐标、具体顺序):
|
|
107
107
|
|
|
108
|
-
1.
|
|
108
|
+
1. **消费步骤**:① 画布取 `layouts.md` 的 `canvas`;② 从页型清单选 archetype,按其 flow / slots / decor / background 原样落版;③ `design.md` frontmatter 的 colors / typography / spacing / rounded / components 作为全局 token,局部 CSS 优先;④ 包内资产复制到项目相对目录后引用,字体使用完整 fallback 栈且不在运行时安装;⑤ 双主题包写明默认主题与 token 前缀切换法。
|
|
109
109
|
2. **资产用法表**:每个资产一行——id、文件路径、用在哪类页、怎么摆(logo 给坐标,背景给首选序——如「封面首选 cover-art,无主视觉需求用 cover-dark」)。
|
|
110
|
-
3.
|
|
110
|
+
3. **强调色族纪律**:强调色族以 `colors` 段和 layout slot CSS 为主。必要时可使用其他颜色,但新增颜色须与模板整体的色相、明度和饱和度关系协调,且不能形成与模板主色竞争的第二强调色。中性色、低彩度辅助色或局部语义色可表达正负、风险、警告、状态、图表序列,但须保持辅助层级;新色不得通过高饱和、高对比、大面积、跨页重复,或用于标题、关键数字、图表主序列、卡片底色、渐变来获得主视觉权重。
|
|
111
|
+
4. **交付检查**:逐页确认色板、字体、版式、背景、资产和 Hard Rules 均来自本包,并检查资源加载、内容溢出与画幅裁切。
|
|
111
112
|
|
|
112
|
-
**Hard Rules 必须包含对应的正向硬规则**(有资产的包):每页放 logo(位置+文件);封面底图必用 cover 资产;版式从 layouts.md
|
|
113
|
+
**Hard Rules 必须包含对应的正向硬规则**(有资产的包):每页放 logo(位置+文件);封面底图必用 cover 资产;版式从 layouts.md 取;以 colors / layout slot CSS 为强调色基准,新增颜色与整体色板协调并保持辅助层级。禁止句只用于无法正向表达的红线,且同句给替代。
|
|
113
114
|
|
|
114
115
|
## 3. `layouts` 段(默认 sidecar)
|
|
115
116
|
|
|
@@ -123,13 +124,21 @@ layouts:
|
|
|
123
124
|
themes: [dark, light] # 深浅孪生合并
|
|
124
125
|
background: {dark: bg-cover-dark, light: bg-cover-light}
|
|
125
126
|
slots:
|
|
126
|
-
- {role: title, box: [
|
|
127
|
-
- {role: logo, box: [
|
|
127
|
+
- {role: title, box: [<x>, <y>, <w>, <h>], type: title, css: "<CSS 声明串>"}
|
|
128
|
+
- {role: logo, box: [<x>, <y>, <w>, <h>], asset: {dark: logo-on-dark, light: logo-on-light}}
|
|
129
|
+
decor:
|
|
130
|
+
- {box: [<x>, <y>, <w>, <h>], geom: ellipse, css: "<CSS 声明串>"}
|
|
128
131
|
confidence: high
|
|
129
132
|
```
|
|
130
133
|
|
|
131
134
|
- **`background` 三形态**:`<asset-id>` / `{<theme>: <asset-id>}` / `{color: <colors-token>}`(`color` 是保留键,主题名禁止叫 color)。`asset` 两形态:`<asset-id>` / `{<theme>: <asset-id>}`。
|
|
132
135
|
- **背景安全扩展**:有真实背景图的 archetype 建议写 `text_safe: [x,y,w,h]`、`avoid: [{box: [x,y,w,h], reason: "..."}]`、`pairing_rule: "..."`。这些是消费约束,不参与封闭枚举;用于避免标题、正文、图表、卡片、表格、时间线及其容器外接矩形覆盖背景视觉主体、强光斑或深色透明区;透明容器也不能跨进禁放区。
|
|
136
|
+
- **流式页型**:内容长度会变化的内容页可用 `flow.regions` 表达纵向区带。`stack` 表达单列顺序,`grid` 表达并列列组,`free` 中的 item 必须带 `box`,用于 logo、页码、页眉和页脚等固定锚点。并列卡片可在 `grid.items` 中使用一层 `{role: group, css, gap, items}`:group 的 `css` 是卡片容器样式,内部 `items` 按顺序排布;不继续嵌套 group。区带可带自己的 `margin: [左, 右]`,覆盖 `flow` 整块的 `margin`(居中卡片组和贴左标题横向范围本就不同);不带则继承整块 `margin`。纵向位置与留白由消费模型结合实际内容决定,不把样张的 `y` 坐标当作流式硬约束。
|
|
137
|
+
- **`decor`(可选)**:这一页无文字的图形骨架——图标托底的圆、卡片、分隔线。每条 `{box, geom, css}`:`box` 定位,`css` 是可直接写进 style 的声明串,`geom` 取源形状的 prst(`ellipse` 另加 `border-radius: 50%`)。圆角以每条 `css` 为准,没有 `border-radius` 就按 `0`;不得因 `geom: roundRect` 自行补圆角,因为 OOXML 的 roundRect 可以有零圆角调节点。层级在背景之上、`slots` 之下;带 `asset` 的槽落在 decor 之上是版式本意,不算重叠。
|
|
138
|
+
- **slot 样式契约**:`box` 只承载 `[x,y,w,h]` 几何;可渲染属性统一放进 `css`,并可直接写入 HTML `style`。PPTX `bodyPr.insets_px` 转成 `box-sizing: border-box; padding: ...`,字号/字重/颜色/水平与垂直对齐/行高/字距/旋转分别转成标准 CSS。禁止在 slot 中输出 `size` / `weight` / `color` / `align` / `valign` / `insets_px` 等旧字段。
|
|
139
|
+
- **文本角色判断**:脚本把实例页及其引用版式中的现有文本槽、几何和 CSS 完整写入草案;`text_roles` 只供模型把这些槽判断为 `title | subtitle | header | footer | body`,不控制槽位去留。判断不清时用 `body`,不归纳模板中不存在的标题、页眉或页脚。
|
|
140
|
+
- **标题结构**:存在合适的模板页型时,沿用其标题层级和局部 CSS,只渲染该页型已有的文字槽;背景中已经可见的固定标题不重复创建文本,该页型没有副标题槽时不新增副标题。没有合适参考时由模型按模板整体视觉判断。
|
|
141
|
+
- **圆角作用域**:`rounded` 只允许表达全档共同的单一圆角档位;零圆角与非零圆角混用、或存在多个非零档位时不输出该全局 token。此时每个 `role: container` / `decor` 的 `css` 是唯一事实源,逐项原样消费,不得归并或推断。
|
|
133
142
|
- **`type` 封闭枚举**:`title | subtitle | body | pic | table | chart | media | slide-number | footer`。大数字/序号走 `type: title`,语义由 `role`(如 `big-number`)承担。
|
|
134
143
|
- **`slots.*.role` 开放不校验**(语义槽位):优先复用已知词表(OOXML ST_SlideLayoutType / Slidev 20 布局 / Google PredefinedLayout,如 big-number、caption、main-point),确无对应再自造。
|
|
135
144
|
- archetype ≤15(内联降级形态 ≤11);深浅孪生合并为一条;版式溯源/母版取舍进 `ref/`。
|
|
@@ -139,14 +148,14 @@ layouts:
|
|
|
139
148
|
|
|
140
149
|
```yaml
|
|
141
150
|
safe-area: # 开放命名 map,可多套边距体系
|
|
142
|
-
content: {top:
|
|
143
|
-
editorial: {left:
|
|
151
|
+
content: {top: <px>, right: <px>, bottom: <px>, left: <px>, applies-to: [content, quote]}
|
|
152
|
+
editorial: {left: <px>, right: <px>, applies-to: [cover, section, closing]}
|
|
144
153
|
confidence: medium
|
|
145
154
|
```
|
|
146
155
|
|
|
147
156
|
冲突裁决:`slots.box` 是实例真值,`safe-area` 是归纳框架,**以 slots.box 为准**。
|
|
148
157
|
|
|
149
|
-
## 5. check_v2 校验(
|
|
158
|
+
## 5. check_v2 校验(19 行:V2-1..V2-16 + V2-R5/R6/R7)
|
|
150
159
|
|
|
151
160
|
check_v1 全部规则原样生效。扫描范围 = 包目录,V2-1/V2-2 跨 design.md + layouts.md 求并集。
|
|
152
161
|
|
|
@@ -165,6 +174,9 @@ check_v1 全部规则原样生效。扫描范围 = 包目录,V2-1/V2-2 跨 des
|
|
|
165
174
|
| V2-11 | 键名命中 YAML 1.1 布尔字面量 | FAIL |
|
|
166
175
|
| V2-12 | `path`/`url`/`color` 恰好存在一个;`color` 仅 background 允许;`full` 仅可伴随 `path` | FAIL |
|
|
167
176
|
| V2-13 | 多主题(`themes` 长度 >1)缺 `default-theme`(只看 design.md frontmatter——冲突以 design.md 为准) | WARN |
|
|
177
|
+
| V2-14 | 版式的 `flow` 与 `slots` 互斥(同时出现 = FAIL);`flow.regions[].kind` 在 `grid`/`stack`/`free` 内,`grid` 必带 `cols` | FAIL |
|
|
178
|
+
| V2-15 | 同段字段自洽:`backgrounds.*` 的 `text_safe` 不得与任一 `avoid` 相交;两者形态须为 `[x, y, w, h]` 四个数且 w/h 为正 | FAIL |
|
|
179
|
+
| V2-16 | slot/flow item 的渲染样式只通过 `css` 承载;出现 `size/weight/color/align/valign/insets_px` 旧键 | FAIL |
|
|
168
180
|
| V2-R5 | sidecar frontmatter 重复 design.md 已有顶层键(`layouts` 载荷键与 `canvas` 除外——canvas 的家就在 sidecar) | WARN |
|
|
169
181
|
| V2-R6 | assets 条目出现审计字段(boxes/aspect/mark/confidence)或 design.md 顶层出现 canvas/canvas-source/theme-mechanism/color-confidence——应移 `ref/audit.yaml` / layouts.md | WARN |
|
|
170
182
|
| V2-R7 | 有 layouts sidecar 指针但正文未出现 `layouts.md` 字样(弱指针,消费者到不了版式数据) | WARN |
|
|
File without changes
|
|
@@ -24,6 +24,11 @@ match-template-name: nestjs-react-fullstack
|
|
|
24
24
|
│ └─ 不需要任何装饰器 ──→ AuthNPaasGuard 默认放行未标 @NeedLogin() 的接口
|
|
25
25
|
│ (`@Public()` 是历史遗留 no-op,不要使用,详见第二节)
|
|
26
26
|
│
|
|
27
|
+
├─ 公开接口会写入应用数据库吗?
|
|
28
|
+
│ ├─ 是,且允许未登录提交 ──→ 接口保持公开;目标 RLS 表补 `FOR INSERT TO anon WITH CHECK (true)`,未登录 POST 验证
|
|
29
|
+
│ │ (anon SELECT / authenticated INSERT 都不能替代 anon INSERT WITH CHECK)
|
|
30
|
+
│ └─ 否,需要当前用户或管理员身份 ──→ 使用 @NeedLogin(),不要开放匿名插入
|
|
31
|
+
│
|
|
27
32
|
├─ 接口返回 401 / 登录跳转不对?
|
|
28
33
|
│ └─ 是 ──→ 第四节 · 401 未授权
|
|
29
34
|
│
|
|
@@ -97,6 +102,7 @@ export class ApiController {
|
|
|
97
102
|
| 手动实例化 `AuthNPaasGuard` | 模块自动注册为全局守卫,无需手动 `@UseGuards()` |
|
|
98
103
|
| 用 `@Public()` 标记公开接口 | `@Public()` 是 no-op;公开接口不需要任何装饰器(opt-in 模式默认放行未标 `@NeedLogin()` 的接口) |
|
|
99
104
|
| 在未标 `@NeedLogin()` 的接口里依赖 `req.userContext.userId` 做业务判断 | 该接口默认放行未登录请求,`userId` 可能为 `undefined`,必须先判空 |
|
|
105
|
+
| 公开接口写 RLS 表但只给 anon SELECT / authenticated INSERT | 允许未登录提交时补 `FOR INSERT TO anon WITH CHECK (true)` 或等价 Drizzle policy,并用未登录 POST 验证;否则加 `@NeedLogin()` |
|
|
100
106
|
| 业务层自行返回 401 实现"未登录跳转" | 用 `@NeedLogin()` 让守卫统一处理,确保 `x-login-url` 头被正确写入 |
|
|
101
107
|
| 自建认证传输:自签 JWT 走自定义 `Authorization` / `X-Auth-Token` 头、自定义 cookie、query token | 这些头在妙搭网关转发时**不透传**(白名单只放 `x-suda-csrf-token` / `x-larkgw-suda-webuser` + 标准头),后端必拿不到 → 401,且易被误诊为网关故障。任何自有账号体系/自分配账号需求,身份一律取 `req.userContext`(网关注入,解析自 `x-larkgw-suda-webuser`)、用 `@NeedLogin()` 认证 + 业务角色表授权,不要自签 JWT 走自定义传输 |
|
|
102
108
|
|
|
@@ -30,7 +30,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
30
30
|
|
|
31
31
|
"开启权限服务"/"设计权限体系"/"规划角色"/"升级鉴权模式"/"开启角色服务" → **必须先调用 `rbac_role_manager` tool 的 DESIGN action**,用户确认后再实施。日常"给某功能加权限"不触发 DESIGN。
|
|
32
32
|
|
|
33
|
-
>
|
|
33
|
+
> **相关技能**:角色的查询、创建、更新等 CLI 操作通过当前沙箱的权限管理 skill 执行。
|
|
34
34
|
|
|
35
35
|
### 403 统一处理(所有模式通用)
|
|
36
36
|
|
|
@@ -60,7 +60,7 @@ if (response.status === 403) throw new Error('无操作权限,请联系管理
|
|
|
60
60
|
2. 统一使用 `useAuth()` 获取 `{ ability, isLoading }`,**禁止** `useAuthAbility` / `useCanRole`(已移除)
|
|
61
61
|
3. 必须处理 `isLoading`——加载期间 `ability.can()` 返回 false,不检查会误判无权限
|
|
62
62
|
4. 创建角色后必须编写鉴权代码,切忌只创建角色不编写代码
|
|
63
|
-
5. **`@CanRole([X])`/`<CanRole roles={[X]}>` 里的 X 必须等于平台实际角色标识(bizID
|
|
63
|
+
5. **`@CanRole([X])`/`<CanRole roles={[X]}>` 里的 X 必须等于平台实际角色标识(bizID),不能凭语义臆造**——创建角色时用当前沙箱权限管理 skill 的建角色命令把标识固定成代码要用的值(该 skill 支持显式指定角色标识),或先用它列出角色取实际标识再写代码。若代码写 `'admin'`/`'reviewer'` 但平台分配给用户的角色标识是 `role_xxx`,两者对不上 → 线上恒 403(日志可见 `用户角色 [role_xxx], 需要 [admin, reviewer]`)
|
|
64
64
|
|
|
65
65
|
### 前端
|
|
66
66
|
|
|
@@ -161,9 +161,9 @@ import { CanRole } from '@lark-apaas/fullstack-nestjs-core';
|
|
|
161
161
|
|
|
162
162
|
| 问题 | 处理方式 |
|
|
163
163
|
|------|---------|
|
|
164
|
-
| 403 错误 |
|
|
165
|
-
| 线上 403 但 dev 正常 / 用户"已授权"仍 403 | 先核对两件事:(1) 角色面板授给用户的角色**标识(bizID)** 是否等于代码 `@CanRole`
|
|
166
|
-
| 开发环境授权不生效 |
|
|
164
|
+
| 403 错误 | 明确告知是无权限报错,确认是否符合预期;用当前沙箱权限管理 skill 核验该用户实际命中的角色(该通道提供角色模拟能力时可用其调试) |
|
|
165
|
+
| 线上 403 但 dev 正常 / 用户"已授权"仍 403 | 先核对两件事:(1) 角色面板授给用户的角色**标识(bizID)** 是否等于代码 `@CanRole` 里的字符串(用当前沙箱权限管理 skill 列角色,取实际标识比对);(2) 是否另搞了一套平台不认的权限(自建角色表 / 引用平台没 create 过的标识 / user_id 名单)绕开平台。两者任一不符即解释 403 |
|
|
166
|
+
| 开发环境授权不生效 | 先用当前沙箱权限管理 skill 核对用户命中的角色与角色成员配置;该通道提供角色模拟能力时引导用户使用 |
|
|
167
167
|
| 用户要求管理角色 | 开发态:`[角色面板](BaseURL?openPanel=auth)`;运行态:按第二节实施 |
|
|
168
168
|
|
|
169
169
|
---
|
package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md
CHANGED
|
@@ -95,7 +95,7 @@ Step 9: 验证(禁止跳过)
|
|
|
95
95
|
|
|
96
96
|
## Step 1: 数据库表 + 数据初始化
|
|
97
97
|
|
|
98
|
-
>
|
|
98
|
+
> 对应权限管理 skill 的 Action:Step 1.1 = `PERM_CREATE_TABLE`,Step 1.2 + 1.3 = `PERM_SEED`。
|
|
99
99
|
|
|
100
100
|
**⛔ 必须严格按以下三步顺序执行,禁止合并步骤或跳步。`authz_role_permissions` 有外键依赖 `authz_permissions`,合并执行会导致映射表静默为空。**
|
|
101
101
|
|