dsh-agentone 1.0.0 → 1.0.3

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/lib/client.js CHANGED
@@ -281,6 +281,382 @@ window.__ModuleLoader__.load({
281
281
  );
282
282
  }
283
283
 
284
+ // ==drive-tab-begin==
285
+ // 飞书云盘侧边栏 tab:iframe 直嵌飞书网页版(飞书不设 frame-ancestors,
286
+ // 2026-10-13 spike 实测)。寻址走 /agentone/api/drive/home——host 侧用
287
+ // lark-cli 找到/创建用户的「我的空间/AgentOne」文件夹(folder token 每
288
+ // 用户独立,不能平台级写死,见 lib/drive.js)。页面交互(上传/下载/
289
+ // 整理)全部是飞书官方 Web UI,本组件只管「落到正确层级」+ 引导三态
290
+ // + 内容自动刷新(轮询 /api/drive/rev 清单指纹,变了重挂载 iframe)
291
+ // + 右下角手动刷新按钮(跨域 iframe 收不到事件,也调不了 reload)。
292
+ // better-sidebar 未装(独立 dsh 用户)时 ctx.get 取不到服务,静默不注册。
293
+ const DRIVE_TAB_ID = 'agentone:feishu-drive';
294
+ const DRIVE_HOME_API = '/agentone/api/drive/home';
295
+
296
+ // better-sidebar 服务引用:页签注册(ctx.inject 回调)与 footer 入口按钮
297
+ // (组件渲染时)两个消费方。组件运行在 React 渲染期,inject 回调早已把
298
+ // 服务存进这里;未装 better-sidebar 时为 null,footer 按钮退化为开设置页。
299
+ let driveSidebarService = null;
300
+ function setDriveSidebarService(value) { driveSidebarService = value || null; }
301
+
302
+ // 寻址响应 → 组件相位(纯函数,测试直接覆盖)。
303
+ const DRIVE_GATE_STATES = [
304
+ 'cli_missing', 'cli_unconfigured', 'cli_unauthorized',
305
+ 'drive_scope_missing', 'drive_app_forbidden',
306
+ ];
307
+
308
+ function driveHomePhase(ok, body) {
309
+ const data = body && typeof body === 'object' ? body : {};
310
+ if (ok && data.state === 'ready' && data.url) {
311
+ return { kind: 'ready', url: data.url, name: data.name || 'AgentOne' };
312
+ }
313
+ if (ok && DRIVE_GATE_STATES.includes(data.state)) {
314
+ return {
315
+ kind: 'gate',
316
+ state: data.state,
317
+ missingScopes: Array.isArray(data.missing_scopes) ? data.missing_scopes : [],
318
+ appMissingScopes: Array.isArray(data.app_missing_scopes) ? data.app_missing_scopes : [],
319
+ };
320
+ }
321
+ return { kind: 'error', message: data.error || '云盘文件夹加载失败,请重试' };
322
+ }
323
+
324
+ const DRIVE_GATE_TEXT = {
325
+ // 首次使用要走一次飞书设备流授权(弹窗里扫码/确认),这层提示必须写明
326
+ cli_unauthorized: '云盘页签需要 lark-cli 的飞书授权才能定位你的「AgentOne」文件夹。首次使用请到 AgentOne 设置页点「去授权」,用手机飞书扫码确认后回来点「重试」。',
327
+ cli_missing: '本机还没有安装 lark-cli,无法定位云盘文件夹。请先在 AgentOne 设置页的「CLI」页签安装,再回来点「重试」。',
328
+ cli_unconfigured: 'lark-cli 还没有完成飞书应用初始化。请先在 AgentOne 设置页的「CLI」页签完成初始化(需要扫码/确认一次),再回来点「重试」。',
329
+ };
330
+
331
+ const scopeList = (scopes) => (scopes.length ? `(缺少:${scopes.join('、')})` : '');
332
+
333
+ /** 三态之外的权限缺口文案:分清「用户重新授权能修」与「只能找管理员」。 */
334
+ function driveGateText(phase) {
335
+ if (phase.state === 'drive_scope_missing') {
336
+ return '你的飞书授权里还没有云盘权限' + scopeList(phase.missingScopes)
337
+ + '。请到 AgentOne 设置页 →「云盘」点「重新授权」,扫码确认后回来点「重试」。';
338
+ }
339
+ if (phase.state === 'drive_app_forbidden') {
340
+ return '飞书应用本身还没有开通云空间权限' + scopeList(phase.appMissingScopes.length ? phase.appMissingScopes : phase.missingScopes)
341
+ + ',重新授权解决不了。请联系管理员在飞书开放平台为应用开通云空间权限并发布版本,然后回来点「重试」。';
342
+ }
343
+ return DRIVE_GATE_TEXT[phase.state] || '云盘暂时不可用,请稍后点「重试」。';
344
+ }
345
+
346
+ const DRIVE_REV_API = '/agentone/api/drive/rev';
347
+ const DRIVE_REV_POLL_MS = 8000;
348
+ const DRIVE_REFRESH_TITLE = '刷新云盘内容';
349
+ const DRIVE_STALE_TITLE = '云盘内容有更新,点击刷新';
350
+
351
+ function FeishuDriveTab() {
352
+ const [phase, setPhase] = React.useState({ kind: 'loading' });
353
+ const [retryTick, setRetryTick] = React.useState(0);
354
+ const [reloadTick, setReloadTick] = React.useState(0);
355
+ const [stale, setStale] = React.useState(false);
356
+ React.useEffect(() => {
357
+ let cancelled = false;
358
+ setPhase({ kind: 'loading' });
359
+ fetch(DRIVE_HOME_API)
360
+ .then(async (resp) => driveHomePhase(resp.ok, await resp.json().catch(() => null)))
361
+ .then((next) => { if (!cancelled) setPhase(next); })
362
+ .catch(() => { if (!cancelled) setPhase({ kind: 'error', message: '网络异常,请重试' }); });
363
+ return () => { cancelled = true; };
364
+ }, [retryTick]);
365
+
366
+ // 自动刷新:iframe 跨域收不到飞书内部事件,只能宿主侧轮询根清单指纹
367
+ // (/drive/rev,服务端 10s 节流)——子文件夹里增删文档会 bump 它在根
368
+ // 清单中的 modified_time(真机实测),指纹一变就重挂载 iframe。首拍只
369
+ // 做基线不重载;rev 为 null(CLI 不可用/资料夹未初始化)按「未知」不动;
370
+ // 页面隐藏时跳过这一拍,省无谓的请求。
371
+ // 重挂载会回到根视图(跨域读不到也恢复不了用户在飞书里点开的层级),
372
+ // 所以检测到用户正浏览云盘(焦点在 iframe 内,activeElement 跨域可读)
373
+ // 时不硬拽——只点亮刷新按钮提示「有更新」,基线不前移;焦点离开后
374
+ // 下一拍自动应用。
375
+ React.useEffect(() => {
376
+ if (phase.kind !== 'ready') return undefined;
377
+ let baseline = null;
378
+ let stopped = false;
379
+ const timer = setInterval(() => {
380
+ if (typeof document !== 'undefined' && document.hidden) return;
381
+ fetch(DRIVE_REV_API)
382
+ .then(async (resp) => (resp.ok ? await resp.json().catch(() => null) : null))
383
+ .then((body) => {
384
+ if (stopped || !body || typeof body.rev !== 'string' || !body.rev) return;
385
+ if (baseline === null) {
386
+ baseline = body.rev;
387
+ return;
388
+ }
389
+ if (body.rev === baseline) return;
390
+ const browsing = typeof document !== 'undefined' && document.activeElement
391
+ && document.activeElement.tagName === 'IFRAME';
392
+ if (browsing) {
393
+ setStale(true);
394
+ return;
395
+ }
396
+ baseline = body.rev;
397
+ setStale(false);
398
+ setReloadTick((t) => t + 1);
399
+ })
400
+ .catch(() => {});
401
+ }, DRIVE_REV_POLL_MS);
402
+ return () => { stopped = true; clearInterval(timer); };
403
+ }, [phase.kind === 'ready' ? phase.url : '']);
404
+
405
+ // 手动刷新:立即重载 iframe 并重新寻址(资料夹被重新定位/收养过也能跟上);
406
+ // 寻址失败保留当前画面不打断
407
+ const refreshDrive = () => {
408
+ setStale(false);
409
+ setReloadTick((t) => t + 1);
410
+ fetch(DRIVE_HOME_API)
411
+ .then(async (resp) => driveHomePhase(resp.ok, await resp.json().catch(() => null)))
412
+ .then((next) => { if (next.kind === 'ready') setPhase(next); })
413
+ .catch(() => {});
414
+ };
415
+
416
+ if (phase.kind === 'loading') {
417
+ return React.createElement('div', {
418
+ style: { padding: '24px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary, #57606a)', fontSize: '13px' },
419
+ }, '正在定位你的飞书云盘文件夹…');
420
+ }
421
+ if (phase.kind === 'ready') {
422
+ // 不设 sandbox:飞书登录态依赖真实第三方 cookie 上下文(spike 实测
423
+ // 无 sandbox 时登录页可渲染、登录后云盘可用)。key 随 reloadTick 变化
424
+ // 即重挂载 = 重载(跨域 iframe 不能调 contentWindow.location.reload)。
425
+ // 用户浏览中时刷新被推迟:顶部横幅 + 胶囊按钮变色双提示(见上面轮询注释)。
426
+ const refreshTitle = stale ? DRIVE_STALE_TITLE : DRIVE_REFRESH_TITLE;
427
+ const banner = React.createElement('div', {
428
+ key: 'stale-banner',
429
+ style: {
430
+ position: 'absolute', top: '0', left: '0', right: '0', zIndex: '1',
431
+ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px',
432
+ padding: '6px 10px', fontSize: '12px',
433
+ background: 'rgba(47, 111, 235, 0.92)', color: '#fff',
434
+ boxShadow: '0 1px 4px rgba(0,0,0,0.12)',
435
+ },
436
+ }, '云盘内容有更新',
437
+ React.createElement('button', {
438
+ type: 'button',
439
+ onClick: refreshDrive,
440
+ style: {
441
+ border: '1px solid rgba(255,255,255,.7)', borderRadius: '6px', padding: '1px 10px',
442
+ fontSize: '12px', cursor: 'pointer',
443
+ background: 'rgba(255,255,255,.16)', color: '#fff',
444
+ },
445
+ }, '立即刷新'));
446
+ const frame = React.createElement('iframe', {
447
+ key: `drive-frame-${reloadTick}`,
448
+ src: phase.url,
449
+ title: '飞书云盘',
450
+ style: {
451
+ border: 'none', width: '100%', height: '100%', minHeight: '320px',
452
+ background: '#fff', display: 'block',
453
+ },
454
+ });
455
+ const pill = React.createElement('button', {
456
+ type: 'button',
457
+ title: refreshTitle,
458
+ 'aria-label': refreshTitle,
459
+ onClick: refreshDrive,
460
+ style: {
461
+ position: 'absolute', right: '12px', bottom: '12px', zIndex: '1',
462
+ display: 'inline-flex', alignItems: 'center', gap: '5px',
463
+ height: '28px', padding: '0 12px',
464
+ border: stale ? 'none' : '1px solid rgba(47, 111, 235, 0.28)',
465
+ borderRadius: '14px', cursor: 'pointer', fontSize: '12px',
466
+ background: stale ? '#2f6feb' : '#eaf1fe',
467
+ color: stale ? '#fff' : '#2f6feb',
468
+ boxShadow: '0 1px 5px rgba(0,0,0,.18)',
469
+ },
470
+ }, React.createElement('svg', {
471
+ viewBox: '0 0 24 24', width: '14', height: '14', 'aria-hidden': 'true', focusable: 'false',
472
+ }, React.createElement('path', {
473
+ d: 'M17.65 6.35A7.95 7.95 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z',
474
+ fill: 'currentColor',
475
+ })),
476
+ React.createElement('span', null, stale ? '有更新,刷新' : '刷新'));
477
+ return React.createElement('div', {
478
+ style: { position: 'relative', width: '100%', height: '100%', minHeight: '320px' },
479
+ }, ...(stale ? [banner] : []), frame, pill);
480
+ }
481
+ const text = phase.kind === 'gate' ? driveGateText(phase) : phase.message;
482
+ const center = {
483
+ padding: '28px 20px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary, #57606a)',
484
+ fontSize: '13px', lineHeight: '1.7',
485
+ };
486
+ const btnPrimary = {
487
+ padding: '7px 18px', fontSize: '13px', borderRadius: '9px', border: 'none', cursor: 'pointer',
488
+ background: '#2f6feb', color: '#fff', marginRight: '8px',
489
+ };
490
+ const btnGhost = { ...btnPrimary, background: 'var(--dsw-alias-fill-secondary, #f2f3f5)', color: 'var(--dsw-alias-label-primary, #3c434d)' };
491
+ return React.createElement('div', { style: center },
492
+ React.createElement('p', { style: { margin: '0 0 16px' } }, text),
493
+ React.createElement('button', { style: btnPrimary, onClick: () => setRetryTick((t) => t + 1) }, '重试'),
494
+ phase.kind === 'gate'
495
+ ? React.createElement('button', {
496
+ style: btnGhost,
497
+ onClick: () => window.open('/agentone/', '_blank', 'noopener'),
498
+ }, '打开设置页')
499
+ : null);
500
+ }
501
+
502
+ function feishuDriveTabDescriptor() {
503
+ return {
504
+ id: DRIVE_TAB_ID,
505
+ title: () => '飞书云盘',
506
+ description: () => '进入你的飞书云盘「AgentOne」文件夹,资料的上传与管理都在飞书界面内完成',
507
+ order: 80,
508
+ single: true,
509
+ // 飞书风格云盘实心图标(用户提供的 云盘.svg,viewBox 1024)
510
+ icon: driveCloudIcon,
511
+ component: () => React.createElement(FeishuDriveTab),
512
+ };
513
+ }
514
+
515
+ // 飞书风格云盘实心图标(用户提供的 云盘.svg,viewBox 1024):侧边栏页签与
516
+ // footer 入口按钮共用。
517
+ function driveCloudIcon(size) {
518
+ return React.createElement('svg', {
519
+ viewBox: '0 0 1024 1024', width: size, height: size,
520
+ 'aria-hidden': 'true', focusable: 'false',
521
+ }, React.createElement('path', {
522
+ d: 'M778.581333 374.101333C766.976 237.226667 651.946667 129.706667 512 129.706667s-254.976 107.52-266.581333 244.394666c-93.184 27.989333-160.768 114.005333-160.768 216.064 0 124.586667 101.034667 225.621333 225.621333 225.621334h404.138667c124.586667 0 225.621333-101.034667 225.621333-225.621334-0.341333-102.058667-68.266667-188.074667-161.450667-216.064z m-128.341333 209.237334c-22.869333 21.845333-62.122667 49.834667-117.418667 58.709333-11.264 1.706667-22.186667 2.730667-32.768 2.730667-72.704 0-126.293333-37.888-142.336-50.858667-18.090667-14.677333-21.162667-41.301333-6.485333-59.392 14.677333-18.090667 41.301333-21.162667 59.392-6.485333 11.605333 9.557333 53.930667 39.253333 108.885333 30.72 35.498667-5.802667 60.416-25.258667 72.021334-36.181334 17.066667-16.042667 43.690667-15.36 59.733333 1.365334 16.725333 16.384 16.042667 43.349333-1.024 59.392z',
523
+ fill: '#635DF7',
524
+ opacity: '.7',
525
+ }));
526
+ }
527
+
528
+ // ── sidebar.footer.action 云盘入口 ─────────────────────────────────────
529
+ // 主界面左栏底部、设置按钮上方的常驻按钮(参照 dsh-ccpg-larkauth 的
530
+ // 「飞书账号」入口,同 slot 同几何口径)。点击打开 better-sidebar 的云盘
531
+ // 页签;未装 better-sidebar 时退化为新窗口开设置页(与页签注册的静默
532
+ // 策略一致,不放死按钮)。宽态带「飞书云盘」文字、收起态只留图标,两态
533
+ // 几何(42px 满行 / 36px 圆钮)逐项对齐官方设置触发器;多个 footer 入口
534
+ // 的纵向堆叠由本插件的 :has() 规则自己兜(不依赖 harness-one 装没装)。
535
+ const FOOTER_STYLE_ID = 'agentone-footer-entry-style';
536
+ function ensureFooterStyle() {
537
+ // 非 DOM 宿主(测试 vm 沙箱/单测加载)直接跳过
538
+ if (typeof document === 'undefined' || !document.getElementById) return;
539
+ if (document.getElementById(FOOTER_STYLE_ID)) return;
540
+ const style = document.createElement('style');
541
+ style.id = FOOTER_STYLE_ID;
542
+ style.textContent = [
543
+ '.aone-drive-entry{box-sizing:border-box;cursor:pointer;width:calc(100% + 4px);height:42px;',
544
+ 'color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:12px;',
545
+ 'flex:none;align-items:center;justify-content:flex-start;gap:8px;margin:4px -2px;',
546
+ 'padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;',
547
+ 'display:flex;overflow:hidden;white-space:nowrap}',
548
+ '.aone-drive-entry:hover{background:var(--dsw-alias-interactive-bg-hover)}',
549
+ '.aone-drive-entry.aone-drive-rail{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;margin:8px 0 10px;padding:0}',
550
+ ':has(> [data-slot="sidebar.footer.action"] .aone-drive-entry){display:flex;flex-direction:column}',
551
+ ].join('');
552
+ document.head.appendChild(style);
553
+ }
554
+
555
+ function DriveFooterEntry(props) {
556
+ const wide = Boolean(props && props.wide);
557
+ ensureFooterStyle();
558
+ const openDrive = () => {
559
+ const sidebar = driveSidebarService;
560
+ if (sidebar && typeof sidebar.openTab === 'function') {
561
+ const sessionId = currentDshSessionId();
562
+ try {
563
+ sidebar.openTab({ type: DRIVE_TAB_ID, title: '飞书云盘' },
564
+ sessionId ? { sessionId } : undefined);
565
+ return;
566
+ } catch {
567
+ /* 页签打不开(旧版本服务/无会话)→ 落到设置页兜底 */
568
+ }
569
+ }
570
+ window.open('/agentone/', '_blank', 'noopener');
571
+ };
572
+ const children = [driveCloudIcon(18)];
573
+ if (wide) children.push(React.createElement('span', null, '飞书云盘'));
574
+ return React.createElement('button', {
575
+ onClick: openDrive,
576
+ title: '飞书云盘(AgentOne 文件夹)',
577
+ className: wide ? 'aone-drive-entry' : 'aone-drive-entry aone-drive-rail',
578
+ }, ...children);
579
+ }
580
+
581
+ // 设置页(/agentone/ 子框架,web 走 iframe、桌面壳走 srcdoc)里点「打开云盘」
582
+ // 时,经 postMessage 让宿主页这边的我们把页签开出来;开不了就回 ok:false,
583
+ // 页面自己退回「新窗口打开飞书链接」。srcdoc 子框架 origin 为 'null'。
584
+ const DRIVE_OPEN_MESSAGE = 'agentone:open-feishu-drive';
585
+ const DRIVE_OPEN_RESULT = 'agentone:open-feishu-drive:result';
586
+
587
+ function isDriveOpenRequest(data) {
588
+ return Boolean(data && typeof data === 'object' && data.type === DRIVE_OPEN_MESSAGE);
589
+ }
590
+
591
+ function driveMessageTrusted(origin) {
592
+ return !origin || origin === 'null' || origin === window.location.origin;
593
+ }
594
+
595
+ /** 当前 dsh 会话 id(openTab 的 scope 用;取不到就让服务自己决定)。 */
596
+ function currentDshSessionId() {
597
+ try {
598
+ const raw = window.localStorage.getItem('dsh.sessions.current');
599
+ const current = raw ? JSON.parse(raw) : null;
600
+ return (current && current.sessionId) || null;
601
+ } catch {
602
+ return null;
603
+ }
604
+ }
605
+
606
+ // 注册并返回 disposer。走 cordis 软依赖 inject(与同宿主 dsh-ccpg-canvasui
607
+ // 注册侧边栏 tab 同口径):better-sidebar 未装时回调不执行——独立 dsh
608
+ // 用户静默无页签;装了则等 betterSidebar 真正 provide 后再注册。
609
+ // 不能用同步 ctx.get:client fiber 的激活顺序不保证 better-sidebar 先就绪,
610
+ // 那个窗口里取到 null 会静默跳过且永不重试(真机事故:侧边栏找不到云盘入口)。
611
+ function registerFeishuDriveTab(ctx) {
612
+ if (typeof ctx.inject !== 'function') return () => {};
613
+ return ctx.inject(['betterSidebar'], (scope) => {
614
+ const sidebar = scope.betterSidebar;
615
+ setDriveSidebarService(sidebar);
616
+ if (!sidebar || typeof sidebar.registerTab !== 'function') return;
617
+ const openDriveTab = () => {
618
+ const sessionId = currentDshSessionId();
619
+ // better-sidebar 0.25 openTab 契约:{type,…} + ({sessionId}) scope
620
+ sidebar.openTab({ type: DRIVE_TAB_ID, title: '飞书云盘' }, sessionId ? { sessionId } : undefined);
621
+ return true;
622
+ };
623
+ const onMessage = (event) => {
624
+ if (!isDriveOpenRequest(event.data) || !driveMessageTrusted(event.origin)) return;
625
+ let ok = false;
626
+ try {
627
+ ok = openDriveTab();
628
+ } catch {
629
+ ok = false;
630
+ }
631
+ const target = event.origin && event.origin !== 'null' ? event.origin : '*';
632
+ try {
633
+ event.source?.postMessage({ type: DRIVE_OPEN_RESULT, ok }, target);
634
+ } catch {
635
+ /* 回执失败不影响已打开/未打开的事实 */
636
+ }
637
+ };
638
+ // 老宿主/测试沙箱可能没有 addEventListener:消息桥是增强,缺了就只注册页签
639
+ const canListen = typeof window.addEventListener === 'function';
640
+ if (canListen) window.addEventListener('message', onMessage);
641
+ const register = () => sidebar.registerTab(feishuDriveTabDescriptor());
642
+ if (typeof scope.effect !== 'function') {
643
+ register();
644
+ return;
645
+ }
646
+ scope.effect(register, 'dsh-agentone: feishu-drive tab');
647
+ if (canListen) {
648
+ scope.effect(() => () => window.removeEventListener('message', onMessage),
649
+ 'dsh-agentone: feishu-drive open bridge');
650
+ }
651
+ // 插件卸载时清掉共享引用:footer 按钮随后点击走设置页兜底,不持有悬空服务
652
+ if (typeof scope.effect === 'function') {
653
+ scope.effect(() => () => setDriveSidebarService(null),
654
+ 'dsh-agentone: feishu-drive service ref');
655
+ }
656
+ });
657
+ }
658
+ // ==drive-tab-end==
659
+
284
660
  // ── 设置导航图标(workbench_fill)─────────────────────────────────────
285
661
  // dsh 设置弹窗的导航图标由 shell(ui-settings-general)按 section id 硬编码,
286
662
  // 注册项没有 icon 选项(仅 models/agent-presets/plugins 有专用图标,其余一律
@@ -333,6 +709,20 @@ window.__ModuleLoader__.load({
333
709
 
334
710
  function apply(ctx) {
335
711
  setupNavIcon();
712
+ // 飞书云盘侧边栏 tab(better-sidebar 缺失时内部静默跳过)
713
+ registerFeishuDriveTab(ctx);
714
+ // 主界面左栏底部云盘入口(order 10 < 飞书账号 20 → 排在它上方、设置按钮之下)。
715
+ // 不依赖 better-sidebar:点击时服务在就开页签,不在退化为新窗口开设置页。
716
+ ctx.slots.inject('sidebar.footer.action', () =>
717
+ ctx.slots.register(
718
+ {
719
+ name: 'sidebar.footer.action',
720
+ id: 'agentone-drive-entry',
721
+ order: 10,
722
+ },
723
+ DriveFooterEntry,
724
+ ),
725
+ );
336
726
  ctx.slots.inject('settings.section', () =>
337
727
  ctx.slots.register(
338
728
  {