@lark-apaas/coding-steering 0.1.32-dev.942e73f → 0.1.32-dev.b12eab4

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.
@@ -0,0 +1,421 @@
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
+ 'theme': attrs_map.get('data-pptx-theme'),
211
+ 'references': [],
212
+ } if slide_root else parent_slide)
213
+ if slide_root:
214
+ self.slides.append(slide)
215
+ styles = inline_styles(attrs_map.get('style'))
216
+ hidden = bool(
217
+ (parent and parent['hidden'])
218
+ or 'hidden' in attrs_map
219
+ or attrs_map.get('aria-hidden', '').lower() == 'true'
220
+ or styles.get('display') == 'none'
221
+ or styles.get('visibility') in ('hidden', 'collapse')
222
+ or styles.get('content-visibility') == 'hidden'
223
+ or css_number(styles.get('width')) == 0
224
+ or css_number(styles.get('height')) == 0
225
+ or (
226
+ css_number(styles.get('opacity')) is not None
227
+ and css_number(styles.get('opacity')) <= 0
228
+ )
229
+ )
230
+ if slide is not None and (urls or attrs_map.get('data-pptx-asset')):
231
+ slide['references'].append({
232
+ 'asset': attrs_map.get('data-pptx-asset'),
233
+ 'hidden': hidden,
234
+ 'slide_root': slide_root,
235
+ 'source': attrs_map.get('src'),
236
+ 'styles': styles,
237
+ 'tag': tag,
238
+ 'urls': urls,
239
+ })
240
+ elif urls:
241
+ self.outside_urls.extend(urls)
242
+ if tag == 'style':
243
+ self._style_depth += 1
244
+ if tag not in VOID_TAGS:
245
+ self._stack.append({
246
+ 'hidden': hidden,
247
+ 'slide': slide,
248
+ 'tag': tag,
249
+ })
250
+
251
+ def handle_startendtag(self, tag, attrs):
252
+ self.handle_starttag(tag, attrs)
253
+ if tag.lower() not in VOID_TAGS:
254
+ self.handle_endtag(tag)
255
+
256
+ def handle_endtag(self, tag):
257
+ tag = tag.lower()
258
+ if tag == 'style' and self._style_depth:
259
+ self._style_depth -= 1
260
+ if tag not in VOID_TAGS and self._stack:
261
+ self._stack.pop()
262
+
263
+ def handle_data(self, data):
264
+ if not self._style_depth:
265
+ return
266
+ self.outside_urls.extend(URL_RE.findall(data))
267
+
268
+
269
+ def validate_layout_assets(pack_dir, html_path, asset_prefix):
270
+ """Return violations of the asset contract declared by each layout."""
271
+ pack = Pack(pack_dir)
272
+ owners = layout_asset_contract(pack)
273
+ known_urls = asset_urls(pack, asset_prefix, owners)
274
+ asset_urls_by_id = {
275
+ asset_id: url
276
+ for url, asset_ids in known_urls.items()
277
+ for asset_id in asset_ids
278
+ }
279
+
280
+ with open(html_path, encoding='utf-8') as stream:
281
+ text = stream.read()
282
+ parser = SlideAssetParser()
283
+ parser.feed(text)
284
+ parser.close()
285
+
286
+ problems = copied_asset_problems(pack, html_path, asset_prefix, owners)
287
+ if not parser.slides:
288
+ problems.append(
289
+ '没有识别到 deck-stage 的直属 slide section,无法核验模板页型')
290
+ return problems
291
+ for url in parser.outside_urls:
292
+ for asset_id in sorted(known_urls.get(normalized_path(url), ())):
293
+ problems.append(
294
+ '模板资产 %s 出现在 slide section 外,无法核验页型归属' % asset_id)
295
+ for number, slide in enumerate(parser.slides, 1):
296
+ layout = slide['layout']
297
+ if not layout:
298
+ problems.append('第 %d 页缺少 data-pptx-layout,无法核验模板资产归属' % number)
299
+ continue
300
+ if layout not in pack.layouts:
301
+ problems.append('第 %d 页声明了不存在的模板页型: %s' % (number, layout))
302
+ continue
303
+ layout_entry = pack.layouts[layout][0]
304
+ explicit_theme = slide['theme']
305
+ default_theme = pack.design.data.get('default-theme')
306
+ active_theme = explicit_theme or default_theme
307
+ if explicit_theme and explicit_theme not in pack.themes:
308
+ problems.append(
309
+ '第 %d 页声明了不存在的模板主题: %s' % (number, explicit_theme))
310
+ continue
311
+ layout_themes = (
312
+ (layout_entry.get('themes') or [])
313
+ if isinstance(layout_entry, dict) else []
314
+ )
315
+ if active_theme and layout_themes and active_theme not in layout_themes:
316
+ problems.append(
317
+ '第 %d 页页型 %s 不支持当前主题 %s;允许主题: %s。'
318
+ '若确需切换,显式声明 data-pptx-theme'
319
+ % (number, layout, active_theme, '、'.join(layout_themes)))
320
+ continue
321
+ expected = layout_asset_instances(
322
+ layout_entry, set(pack.assets), pack.canvas)
323
+ actual = []
324
+ for reference in slide['references']:
325
+ referenced_ids = set()
326
+ normalized_urls = [normalized_path(url) for url in reference['urls']]
327
+ for url in normalized_urls:
328
+ referenced_ids.update(known_urls.get(url, ()))
329
+ asset_id = reference['asset']
330
+ if not asset_id:
331
+ for referenced_id in sorted(referenced_ids):
332
+ problems.append(
333
+ '第 %d 页模板资产 %s 缺少 data-pptx-asset 实例标记'
334
+ % (number, referenced_id))
335
+ continue
336
+ if asset_id not in pack.assets:
337
+ problems.append(
338
+ '第 %d 页声明了不存在的模板资产: %s' % (number, asset_id))
339
+ continue
340
+ expected_url = asset_urls_by_id.get(asset_id)
341
+ uses_expected_source = (
342
+ len(normalized_urls) == 1
343
+ and normalized_urls[0] == expected_url
344
+ and (
345
+ reference['tag'] != 'img'
346
+ or (
347
+ reference['source']
348
+ and normalized_path(reference['source']) == expected_url
349
+ )
350
+ )
351
+ )
352
+ if not uses_expected_source:
353
+ problems.append(
354
+ '第 %d 页固定实例 %s 未引用对应的 PPTX 原素材'
355
+ % (number, asset_id))
356
+ if reference['hidden']:
357
+ problems.append(
358
+ '第 %d 页固定实例 %s 不可隐藏' % (number, asset_id))
359
+ actual.append({
360
+ 'asset': asset_id,
361
+ 'box': element_box(reference, pack.canvas),
362
+ })
363
+
364
+ used_asset_counts = Counter(instance['asset'] for instance in actual)
365
+ for asset_id in sorted(used_asset_counts):
366
+ if layout not in owners.get(asset_id, set()):
367
+ allowed = '、'.join(sorted(owners.get(asset_id) or ())) or '(无)'
368
+ problems.append(
369
+ '第 %d 页页型 %s 不得使用 %s;只允许: %s'
370
+ % (number, layout, asset_id, allowed))
371
+ unmatched = list(actual)
372
+ for asset_id, _, expected_box in expected:
373
+ matching_index = next((
374
+ index for index, instance in enumerate(unmatched)
375
+ if instance['asset'] == asset_id
376
+ and boxes_match(instance['box'], expected_box)
377
+ ), None)
378
+ if matching_index is not None:
379
+ unmatched.pop(matching_index)
380
+ continue
381
+ same_asset = next((
382
+ instance for instance in unmatched
383
+ if instance['asset'] == asset_id
384
+ ), None)
385
+ if same_asset:
386
+ problems.append(
387
+ '第 %d 页固定实例 %s 的位置尺寸必须为 %s,当前为 %s'
388
+ % (number, asset_id, expected_box, same_asset['box']))
389
+ unmatched.remove(same_asset)
390
+ else:
391
+ problems.append(
392
+ '第 %d 页页型 %s 缺少固定实例 %s,位置尺寸应为 %s'
393
+ % (number, layout, asset_id, expected_box))
394
+ for instance in unmatched:
395
+ if layout in owners.get(instance['asset'], set()):
396
+ problems.append(
397
+ '第 %d 页页型 %s 额外使用了固定实例 %s'
398
+ % (number, layout, instance['asset']))
399
+ return problems
400
+
401
+
402
+ def main(argv=None):
403
+ parser = argparse.ArgumentParser(
404
+ description='Verify that generated deck HTML honors PPTX layout asset bindings.')
405
+ parser.add_argument('pack_dir')
406
+ parser.add_argument('html_path')
407
+ parser.add_argument('--asset-prefix', required=True)
408
+ args = parser.parse_args(argv)
409
+
410
+ problems = validate_layout_assets(args.pack_dir, args.html_path, args.asset_prefix)
411
+ if not problems:
412
+ print('PPTX_LAYOUT_ASSETS: PASS')
413
+ return 0
414
+ print('PPTX_LAYOUT_ASSETS: FAIL count=%d' % len(problems))
415
+ for problem in problems:
416
+ print('[layoutAssets] %s' % problem)
417
+ return 1
418
+
419
+
420
+ if __name__ == '__main__':
421
+ sys.exit(main())
@@ -1,187 +1,11 @@
1
1
  #!/usr/bin/env python3
2
- """Verify that generated slides use template logos only on owning archetypes."""
3
- import argparse
4
- import posixpath
5
- import re
2
+ """Backward-compatible entrypoint for PPTX layout asset verification."""
6
3
  import sys
7
- from html.parser import HTMLParser
8
- from urllib.parse import urlsplit
9
4
 
10
- from check_v2 import Pack
5
+ from verify_layout_assets import main, validate_layout_assets
11
6
 
12
7
 
13
- URL_RE = re.compile(r'url\(\s*[\'"]?([^\'")\s]+)', re.I)
14
- VOID_TAGS = {
15
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
16
- 'link', 'meta', 'param', 'source', 'track', 'wbr',
17
- }
18
-
19
-
20
- def normalized_path(value):
21
- path = urlsplit(value).path.replace('\\', '/')
22
- return posixpath.normpath(path).lstrip('./')
23
-
24
-
25
- def asset_urls(pack, asset_prefix):
26
- prefix = normalized_path(asset_prefix).rstrip('/')
27
- urls = {}
28
- for asset_id, (entry, _) in pack.assets.items():
29
- if not isinstance(entry, dict) or entry.get('kind') != 'logo':
30
- continue
31
- path = entry.get('path')
32
- if not isinstance(path, str) or not path:
33
- continue
34
- relative = normalized_path(path)
35
- if relative.startswith('assets/'):
36
- relative = relative[len('assets/'):]
37
- urls[posixpath.join(prefix, relative)] = asset_id
38
- return urls
39
-
40
-
41
- def contains_asset(value, asset_id):
42
- if isinstance(value, dict):
43
- return (value.get('asset') == asset_id
44
- or any(contains_asset(child, asset_id) for child in value.values()))
45
- if isinstance(value, list):
46
- return any(contains_asset(child, asset_id) for child in value)
47
- return False
48
-
49
-
50
- def logo_owners(pack):
51
- owners = {}
52
- for asset_url, asset_id in asset_urls(pack, '').items():
53
- del asset_url
54
- owners[asset_id] = {
55
- name
56
- for name, (layout, _) in pack.layouts.items()
57
- if contains_asset(layout, asset_id)
58
- }
59
- return owners
60
-
61
-
62
- def urls_from_attrs(attrs):
63
- urls = []
64
- for key, value in attrs:
65
- if not value:
66
- continue
67
- if key.lower() in ('src', 'href'):
68
- urls.append(value)
69
- elif key.lower() == 'srcset':
70
- urls.extend(item.strip().split(' ', 1)[0] for item in value.split(','))
71
- elif key.lower() == 'style':
72
- urls.extend(URL_RE.findall(value))
73
- return urls
74
-
75
-
76
- class SlideAssetParser(HTMLParser):
77
- def __init__(self):
78
- super().__init__()
79
- self.slides = []
80
- self._tags = []
81
- self._sections = []
82
- self._active_slides = []
83
- self._style_depth = 0
84
- self.outside_urls = []
85
-
86
- def handle_starttag(self, tag, attrs):
87
- tag = tag.lower()
88
- urls = urls_from_attrs(attrs)
89
- parent = self._tags[-1] if self._tags else None
90
- if tag == 'section':
91
- slide = (
92
- {'layout': dict(attrs).get('data-pptx-layout'), 'urls': urls}
93
- if parent == 'deck-stage' else None
94
- )
95
- self._sections.append(slide)
96
- if slide is not None:
97
- self._active_slides.append(slide)
98
- elif self._active_slides:
99
- self._active_slides[-1]['urls'].extend(urls)
100
- else:
101
- self.outside_urls.extend(urls)
102
- if tag == 'style':
103
- self._style_depth += 1
104
- if tag not in VOID_TAGS:
105
- self._tags.append(tag)
106
-
107
- def handle_startendtag(self, tag, attrs):
108
- if self._active_slides:
109
- self._active_slides[-1]['urls'].extend(urls_from_attrs(attrs))
110
- else:
111
- self.outside_urls.extend(urls_from_attrs(attrs))
112
-
113
- def handle_endtag(self, tag):
114
- tag = tag.lower()
115
- if tag == 'style' and self._style_depth:
116
- self._style_depth -= 1
117
- if tag == 'section' and self._sections:
118
- slide = self._sections.pop()
119
- if slide is not None:
120
- self.slides.append(slide)
121
- self._active_slides.pop()
122
- if tag not in VOID_TAGS and self._tags:
123
- self._tags.pop()
124
-
125
- def handle_data(self, data):
126
- if not self._style_depth:
127
- return
128
- self.outside_urls.extend(URL_RE.findall(data))
129
-
130
-
131
- def validate_logo_scope(pack_dir, html_path, asset_prefix):
132
- """Return every template-logo placement that does not match the slide archetype."""
133
- pack = Pack(pack_dir)
134
- known_urls = asset_urls(pack, asset_prefix)
135
- owners = logo_owners(pack)
136
- if not known_urls:
137
- return []
138
-
139
- with open(html_path, encoding='utf-8') as stream:
140
- text = stream.read()
141
- parser = SlideAssetParser()
142
- parser.feed(text)
143
- parser.close()
144
-
145
- problems = []
146
- for url in parser.outside_urls:
147
- asset_id = known_urls.get(normalized_path(url))
148
- if asset_id:
149
- problems.append(
150
- '模板 logo %s 出现在 slide section 外,无法核验页型归属' % asset_id)
151
- for number, slide in enumerate(parser.slides, 1):
152
- layout = slide['layout']
153
- if not layout:
154
- problems.append('第 %d 页缺少 data-pptx-layout,无法核验模板 logo 归属' % number)
155
- continue
156
- if layout not in pack.layouts:
157
- problems.append('第 %d 页声明了不存在的模板页型: %s' % (number, layout))
158
- continue
159
- for url in slide['urls']:
160
- asset_id = known_urls.get(normalized_path(url))
161
- if asset_id and layout not in owners.get(asset_id, set()):
162
- allowed = '、'.join(sorted(owners.get(asset_id) or ())) or '(无)'
163
- problems.append(
164
- '第 %d 页页型 %s 不得使用 %s;只允许: %s'
165
- % (number, layout, asset_id, allowed))
166
- return problems
167
-
168
-
169
- def main(argv=None):
170
- parser = argparse.ArgumentParser(
171
- description='Verify that generated deck HTML places PPTX logos only on owning layouts.')
172
- parser.add_argument('pack_dir')
173
- parser.add_argument('html_path')
174
- parser.add_argument('--asset-prefix', required=True)
175
- args = parser.parse_args(argv)
176
-
177
- problems = validate_logo_scope(args.pack_dir, args.html_path, args.asset_prefix)
178
- if not problems:
179
- print('PPTX_LOGO_SCOPE: PASS')
180
- return 0
181
- print('PPTX_LOGO_SCOPE: FAIL count=%d' % len(problems))
182
- for problem in problems:
183
- print('[logoScope] %s' % problem)
184
- return 1
8
+ validate_logo_scope = validate_layout_assets
185
9
 
186
10
 
187
11
  if __name__ == '__main__':
@@ -19,7 +19,7 @@ match-template-name: nestjs-react-fullstack
19
19
  项目提供了dataloom SDK的文件服务,用于文件的上传,下载,删除,列出bucket中文件,创建临时签名url等能力。
20
20
 
21
21
  ## 使用注意
22
- - **入口边界**:本 SDK 是**应用前端代码**读写应用存储的唯一入口。Agent 自己在对话 / 开发中上传、灌数据或调试文件请用 `miaoda file` CLI(见 `miaoda-file` skill)——二者操作同一个应用存储桶,但 **`miaoda file` CLI 禁止写进页面代码**,页面里一律用本 SDK。
22
+ - **入口边界**:本 SDK 是**应用前端代码**读写应用存储的唯一入口。Agent 自己在对话 / 开发中上传、灌数据或调试文件,先加载应用文件存储操作 skill(按「应用文件存储 / 文件上传 / 文件下载」召回),用其 CLI 命令操作,具体命令一律以该 skill 为准——二者操作同一个应用存储桶,但**该 CLI 命令只供 Agent 在沙箱终端使用,禁止写进页面代码**,页面里一律用本 SDK。
23
23
  - @lark-apaas/client-toolkit/dataloom 这个SDK只适用于前端调用,禁止在服务端调用
24
24
  - 上传成功后,最重要的返回值是 `data.download_url`。需要将此URL保存到你的业务数据库中
25
25
  - **⚠️ 场景区分(重要)**:`dataloom.storage` 仅适用于需要持久化存储文件或获取 `download_url` 保存到数据库的场景。如果文件仅作为插件输入(传给 `capabilityClient`),**必须直接传 File/Blob 对象,禁止先走 dataloom 上传再传 URL**;插件调用(capability)不属于 dataloom,详见 plugin-guide