@numa-tech/numa 1.18.0 → 1.19.0

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.
@@ -1,24 +1,51 @@
1
+ import { OSS_REFRESH_ICON, OSS_COPY_ICON } from "./web-icons.js";
1
2
  /** Self-contained local assets: no CDN, remote fonts, analytics or credential-bearing markup. */
2
3
  export const OSS_WEB_HTML = String.raw `<!doctype html>
3
4
  <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4
5
  <title>Numa · OSS 文件</title><link rel="icon" href="data:,"><link rel="stylesheet" href="/app.css"><script src="/app.js" defer></script></head>
5
- <body><header class="topbar"><a class="brand" href="/">numa<span> / OSS</span></a><span class="local"><i></i> 本机连接</span><span class="topnote">文件工作区</span></header>
6
- <div class="authorization"><div class="grant-field"><label for="grant">当前授权</label><select id="grant" aria-label="选择 OSS Grant"><option>正在读取授权…</option></select><button id="grants-refresh" class="quiet icon-button" title="刷新授权列表" aria-label="刷新授权列表"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 7v5h-5M4 17v-5h5"/><path d="M6.1 6.1A8 8 0 0 1 19.5 9L20 12M4 12l.5 3A8 8 0 0 0 17.9 17.9"/></svg></button></div>
6
+ <body><header class="topbar"><a class="brand" href="/">numa<span> / OSS</span></a><span class="local"><i></i> 本机连接</span><span class="topnote">文件工作区</span><button id="tasks-toggle" aria-controls="tasks-panel" aria-expanded="false">传输任务 <span id="tasks-count">0</span></button></header>
7
+ <div class="authorization"><div class="grant-field"><label for="grant">当前授权</label><select id="grant" aria-label="选择 OSS Grant"><option>正在读取授权…</option></select><button id="grants-refresh" class="quiet icon-button" title="刷新授权列表" aria-label="刷新授权列表">${OSS_REFRESH_ICON}</button></div>
7
8
  <div class="session-controls"><div class="expiry"><span>STS 剩余时间</span><strong id="countdown" aria-live="off">--:--</strong></div><label class="check"><input id="auto-renew" type="checkbox" checked>自动续期</label><button id="renew">立即续期</button></div></div>
8
9
  <div id="notice" class="notice" role="status" hidden></div>
9
10
  <div class="workspace"><aside class="sidebar"><div class="eyebrow">存储位置</div><button id="root" class="root-button"><span class="folder-icon"></span><span id="bucket">正在连接</span></button><div class="scope-label">授权目录</div><div id="scope" class="scope">—</div><div class="sidebar-bottom"><span id="permission">正在读取权限</span><p>文件操作仅限当前授权范围。<br>关闭 CLI 将断开本机连接。</p></div></aside>
10
- <main class="files"><div class="page-heading"><div><div class="eyebrow">OBJECT STORAGE</div><h1>文件</h1></div><button id="upload" class="primary" disabled>+ 上传文件</button><input id="file-input" type="file" multiple hidden></div>
11
- <div class="location"><button id="up" class="quiet" title="返回上级目录" aria-label="返回上级目录">↑</button><nav id="breadcrumbs" aria-label="文件路径"></nav><button id="refresh" class="quiet">刷新</button></div>
11
+ <main class="files"><div class="page-heading"><div><div class="eyebrow">OBJECT STORAGE</div><h1>文件</h1></div><div class="upload-actions"><button id="upload-folder" disabled>上传文件夹</button><button id="upload" class="primary" disabled>+ 上传文件</button></div></div>
12
+ <div class="location"><button id="up" class="quiet" title="返回上级目录" aria-label="返回上级目录">↑</button><nav id="breadcrumbs" aria-label="文件路径"></nav><button id="refresh" class="quiet icon-button" title="刷新文件列表" aria-label="刷新文件列表">${OSS_REFRESH_ICON}</button></div>
12
13
  <div class="toolbar"><input id="search" type="search" placeholder="筛选已加载的文件与目录…" aria-label="筛选已加载的文件与目录"><span id="selection-count"></span><button id="download-selected" disabled>下载所选</button><button id="delete-selected" class="danger" disabled>删除所选</button></div>
13
- <div class="table-wrap"><table><thead><tr><th class="check-cell"><input id="select-all" type="checkbox" aria-label="选择已显示文件"></th><th>名称</th><th class="size-cell">大小</th><th class="date-cell">修改时间</th></tr></thead><tbody id="rows"></tbody></table><div id="empty" class="empty">正在连接当前授权…</div></div>
14
+ <div class="table-wrap"><table><thead><tr><th class="check-cell"><input id="select-all" type="checkbox" aria-label="选择已显示的文件与目录"></th><th>名称</th><th class="size-cell">大小</th><th class="date-cell">修改时间</th></tr></thead><tbody id="rows"></tbody></table><div id="empty" class="empty">正在连接当前授权…</div></div>
14
15
  <footer class="list-footer"><span id="list-summary">—</span><button id="more" hidden>加载更多</button></footer>
15
- <div id="transfer" class="transfer" hidden><div><strong id="transfer-title">上传中</strong><button id="cancel-upload" class="quiet">取消上传</button></div><progress id="progress" max="100" value="0"></progress><span id="transfer-detail"></span></div>
16
+
16
17
  </main><aside class="preview-panel"><div class="preview-heading"><h2>文件预览</h2><button id="preview-close" class="quiet" aria-label="关闭预览">×</button></div><div id="preview" class="preview-body"><div class="preview-empty">选择一个文件<br><small>查看内容与文件信息</small></div></div><div class="preview-actions"><button id="download" disabled>下载文件</button><button id="delete" class="danger" disabled>删除文件</button></div></aside></div>
17
18
  <dialog id="delete-dialog"><h2>删除所选文件?</h2><p>此操作会删除远程 OSS 对象,不能在此页面撤销。</p><ul id="delete-list"></ul><p id="delete-target" class="scope"></p><div class="dialog-actions"><button id="delete-cancel">取消</button><button id="delete-confirm" class="destructive">确认删除</button></div></dialog>
19
+ <section id="tasks-panel" class="tasks-panel" aria-labelledby="tasks-heading" hidden><div class="tasks-heading"><h2 id="tasks-heading">传输任务</h2><button id="tasks-close" class="quiet" aria-label="关闭传输任务">×</button></div><p class="tasks-note">由本机 CLI 持续传输,关闭浏览器后仍会继续。CLI 重启后,未完成任务会暂停,需手动恢复。</p><p id="tasks-connection" role="status" hidden></p><div id="tasks-list" class="tasks-list"></div><p id="tasks-empty" class="tasks-note">暂无任务。选择文件或文件夹开始传输。</p></section>
20
+ <template id="copy-icon">${OSS_COPY_ICON}</template>
18
21
  </body></html>`;
19
22
  export const OSS_WEB_CSS = String.raw `
20
23
  .icon-button{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;min-width:34px;min-height:34px}
21
24
  :root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;color:#23322e;background:#f5f7f5;font-size:14px;font-synthesis:none}*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button,select{cursor:pointer}button{border:1px solid #d8e0db;background:white;color:#344b41;border-radius:6px;padding:8px 12px;white-space:nowrap}button:hover:not(:disabled){background:#eff5f1;border-color:#a5bcae}button:disabled{opacity:.4;cursor:not-allowed}input:focus-visible,button:focus-visible,select:focus-visible{outline:2px solid #16805a;outline-offset:2px}input[type=checkbox]{accent-color:#197854;width:15px;height:15px;cursor:pointer}input[type=search]{border:1px solid #dbe3de;border-radius:6px;padding:9px 12px;min-width:160px;background:#fafcfb;color:#23322e}select{border:0;background:transparent;color:#263d31;max-width:100%;padding:6px 24px 6px 0;font-weight:550}h1,h2,p{margin:0}h1{font-size:27px;letter-spacing:-.7px;font-weight:600}h2{font-size:15px;font-weight:600}.topbar{height:62px;padding:0 26px;background:#20372e;color:#ecf4ed;display:flex;align-items:center;gap:20px}.brand{font-size:24px;letter-spacing:-1px;font-weight:650;color:inherit;text-decoration:none}.brand span{font-size:16px;font-weight:400;letter-spacing:.5px;color:#a9c0b0;margin-left:12px}.local{font-size:11px;color:#cee0d5;display:flex;align-items:center;gap:6px}.local i{height:6px;width:6px;background:#8edaac;border-radius:50%}.topnote{margin-left:auto;color:#aac0b2;font-size:12px;letter-spacing:2px}.authorization{display:flex;align-items:center;justify-content:space-between;gap:24px;background:#fff;border-bottom:1px solid #dbe3de;padding:16px 26px}.grant-field{min-width:0;display:flex;align-items:center;gap:12px;flex:1}.grant-field label{font-size:12px;color:#7a8c80;white-space:nowrap}.session-controls{display:flex;align-items:center;gap:18px;flex-shrink:0}.expiry{display:flex;align-items:center;gap:10px;font-size:11px;color:#718076}.expiry strong{font-family:ui-monospace,SFMono-Regular,monospace;color:#2b674c;font-size:20px;font-weight:500;min-width:62px}.expiry strong.warning{color:#b45309}.expiry strong.expired{color:#bf403d}.check{display:flex;align-items:center;gap:5px;font-size:12px;white-space:nowrap}.quiet{background:transparent;border-color:transparent;color:#718577;padding:6px 8px}.primary{color:white;background:#227b54;border-color:#227b54}.primary:hover:not(:disabled){background:#196140;color:white}.danger{color:#9f4b40}.destructive{color:white;background:#a7473d;border-color:#a7473d}.notice{margin:0;padding:10px 26px;background:#fff4df;border-bottom:1px solid #ead9b8;color:#845422;font-size:13px;line-height:1.6}.notice.success{background:#e9f6ec;color:#266342;border-color:#c9e3d0}.workspace{display:grid;grid-template-columns:205px minmax(420px,1fr) 310px;min-height:calc(100vh - 137px)}.sidebar{border-right:1px solid #dbe3de;padding:30px 16px;display:flex;flex-direction:column;background:#f0f4f0;min-width:0}.eyebrow{font-size:10px;letter-spacing:1.7px;color:#89988d;font-weight:600}.sidebar .eyebrow{padding-left:10px}.root-button{display:flex;align-items:center;gap:10px;margin-top:18px;white-space:normal;text-align:left;overflow-wrap:anywhere;background:#e1ebe2;border:0;color:#24573d;font-size:13px}.folder-icon{display:inline-block;width:16px;height:11px;background:#9fbba1;border-radius:2px;position:relative;flex-shrink:0;margin-top:3px}.folder-icon:before{content:"";position:absolute;left:0;top:-3px;width:7px;height:4px;border-radius:2px 2px 0 0;background:inherit}.scope-label{font-size:11px;color:#86978a;margin:23px 10px 9px}.scope{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;color:#637c6a;overflow-wrap:anywhere;line-height:1.7}.sidebar>.scope{padding:0 10px}.sidebar-bottom{margin-top:auto;padding:40px 10px 0;color:#607e68;font-size:12px}.sidebar-bottom p{margin-top:12px;line-height:1.8;color:#8b9b8e;font-size:11px}.files{background:white;min-width:0;padding:30px 28px 0;display:flex;flex-direction:column}.page-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}.page-heading .eyebrow{margin-bottom:8px}.location{display:flex;align-items:center;gap:6px;border-top:1px solid #e8ede9;border-bottom:1px solid #e8ede9;padding:9px 0;min-width:0}.location nav{display:flex;align-items:center;flex-wrap:wrap;flex:1;gap:2px;min-width:0}.location nav button{padding:4px 7px;border:0;font-size:12px;max-width:220px;overflow:hidden;text-overflow:ellipsis}.location nav span{color:#c1ccc4}.toolbar{display:flex;gap:8px;align-items:center;margin:18px 0}.toolbar input[type=search]{flex:1;min-width:0}.toolbar button{font-size:12px;padding:7px 9px}#selection-count{font-size:11px;color:#819286;white-space:nowrap}.table-wrap{overflow-x:auto;min-height:290px}table{width:100%;border-collapse:collapse;table-layout:fixed}th{text-align:left;font-size:11px;color:#8a978e;font-weight:500;border-bottom:1px solid #e3e9e4;padding:11px 9px}td{padding:12px 9px;border-bottom:1px solid #f0f3f0;font-size:12px;color:#79897e}.check-cell{width:30px;padding-left:0}.size-cell{width:82px;text-align:right;font-variant-numeric:tabular-nums}.date-cell{width:145px;text-align:right}tr:hover td{background:#f7faf7}tr.active td{background:#edf5ee}td .entry{border:0;background:transparent;padding:0;display:flex;align-items:center;gap:10px;max-width:100%;color:#354e3e;font-size:13px;text-align:left}td .entry span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-icon{display:inline-block;width:13px;height:16px;border:1px solid #a8b8ad;background:#f5f8f5;border-radius:2px;flex-shrink:0}.empty{text-align:center;padding:65px 20px;color:#8b9b8f;line-height:1.9}.list-footer{border-top:1px solid #e9eee9;display:flex;align-items:center;justify-content:space-between;color:#92a095;font-size:11px;padding:13px 0;margin-top:auto;min-height:48px}.preview-panel{border-left:1px solid #dbe3de;display:flex;flex-direction:column;padding:29px 20px 20px;background:#f8faf7;min-width:0}.preview-heading{display:flex;justify-content:space-between;align-items:center}.preview-heading h2{font-size:13px;color:#61776a}.preview-body{padding-top:25px;flex:1;min-width:0}.preview-empty{text-align:center;color:#859889;padding:80px 0;line-height:2}.preview-empty small{color:#a0aca2;font-size:11px}.preview-body h3{font-size:15px;line-height:1.7;overflow-wrap:anywhere;font-weight:500;margin:0 0 18px}.preview-body dl{font-size:11px;display:grid;grid-template-columns:60px minmax(0,1fr);gap:10px;margin:20px 0;color:#91a094}.preview-body dd{margin:0;color:#607968;overflow-wrap:anywhere}.preview-body pre{font:11px/1.8 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;overflow-wrap:anywhere;max-height:50vh;overflow:auto;background:#edf2ec;padding:12px;border-radius:5px;color:#48654e}.preview-body img{max-width:100%;max-height:40vh;object-fit:contain;border-radius:4px}.preview-body p{font-size:12px;line-height:1.8;color:#8c9a8f}.preview-actions{display:flex;gap:8px;padding-top:20px}.preview-actions button{flex:1;font-size:12px}.transfer{border-top:1px solid #dce8df;padding:14px 0;font-size:12px}.transfer>div{display:flex;align-items:center;justify-content:space-between}.transfer progress{width:100%;height:5px;accent-color:#227b54;margin:10px 0}.transfer span{font-size:11px;color:#7b9082}dialog{border:1px solid #d4ded6;border-radius:10px;padding:26px;max-width:min(480px,90vw);color:#344c3c;box-shadow:0 20px 80px #182c2530}dialog::backdrop{background:#1c322747}dialog h2{font-size:19px;margin-bottom:15px}dialog p{font-size:13px;line-height:1.8;color:#879489}dialog ul{max-height:180px;overflow:auto;padding-left:20px;font-size:12px;line-height:1.9;overflow-wrap:anywhere}.dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:24px}[hidden]{display:none!important}@media(min-width:1500px){.workspace{grid-template-columns:235px minmax(500px,1fr) 370px}.files{padding-left:38px;padding-right:38px}}@media(max-width:1100px){.workspace{grid-template-columns:160px minmax(360px,1fr) 260px}.files{padding:24px 18px 0}.authorization{flex-wrap:wrap;gap:10px}.grant-field{flex-basis:100%}.session-controls{margin-left:auto}.date-cell{width:110px}.toolbar{flex-wrap:wrap}.toolbar input[type=search]{flex-basis:100%}}@media(max-width:820px){.workspace{grid-template-columns:1fr}.sidebar{display:none}.preview-panel{border-left:0;border-top:1px solid #dbe3de}.preview-empty{padding:20px}.files{min-height:60vh}.authorization{padding:14px 18px}.grant-field{gap:7px;flex-wrap:wrap}.grant-field select{flex:1;min-width:180px}.session-controls{width:100%;justify-content:space-between;gap:8px}.expiry{gap:6px}.topbar{padding:0 18px}.topnote{display:none}.date-cell{width:110px}.preview-body pre{max-height:280px}.preview-actions{max-width:360px}}
25
+ /* Independent scrolling keeps file actions within the current viewport. */
26
+ body{height:100vh;height:100dvh;display:flex;flex-direction:column;overflow:hidden}
27
+ .topbar,.authorization,.notice{flex-shrink:0}.notice{max-height:20vh;overflow:auto}
28
+ .workspace{flex:1;min-height:0;overflow:hidden}.sidebar{min-height:0;overflow:auto}
29
+ .files{min-height:0;overflow:hidden}.files>:not(.table-wrap){flex-shrink:0}
30
+ .table-wrap{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain}
31
+ .table-wrap thead{position:sticky;top:0;background:#fff;z-index:1}
32
+ .list-footer{margin-top:0;flex-shrink:0}.preview-panel{min-height:0;overflow:hidden}
33
+ .preview-heading,.preview-actions{flex-shrink:0}.preview-body{min-height:0;overflow:auto;overscroll-behavior:contain;padding-right:6px}
34
+ .preview-actions{border-top:1px solid #dbe3de;margin-top:16px;padding-top:16px;background:#f8faf7}
35
+ .preview-body h4{font-size:12px;font-weight:600;margin:24px 0 12px;color:#496553}
36
+ .preview-body dl{grid-template-columns:78px minmax(0,1fr)}.preview-body dt{overflow-wrap:anywhere}
37
+ .preview-body .metadata-list{grid-template-columns:minmax(0,1fr);gap:5px}.metadata-list dd{padding-bottom:9px}
38
+ .copy-field{display:flex;align-items:flex-start;gap:6px;min-width:0}.copy-field>span{min-width:0;overflow-wrap:anywhere}
39
+ .copy-button{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;padding:2px;width:24px;height:24px;margin-top:-3px}
40
+ .copy-button svg{width:15px;height:15px}.copy-button.copied{color:#18764e;background:#e1f2e7}
41
+ .copy-field a{color:#197854;text-underline-offset:3px;line-height:1.7}.copy-manual{width:100%;font:inherit}
42
+ .clipboard-fallback{position:fixed;left:0;top:0;opacity:0;pointer-events:none}
43
+ .icon-button{color:#526f5e}.icon-button svg{display:block}.is-loading svg{animation:oss-refresh-spin .8s linear infinite}
44
+ @keyframes oss-refresh-spin{to{transform:rotate(360deg)}}
45
+ @media(prefers-reduced-motion:reduce){.is-loading svg{animation:none}.is-loading{opacity:.55}}
46
+ @media(max-width:820px){.workspace{display:flex;flex-direction:column;overflow:auto}.files{flex:none;height:58dvh;min-height:330px}.preview-panel{flex:none;height:45dvh;min-height:270px}.preview-body pre{max-height:none}}
47
+
48
+ .upload-actions{display:flex;gap:8px}.tasks-panel{position:fixed;right:20px;bottom:20px;top:155px;width:min(480px,calc(100vw - 32px));z-index:5;background:#fff;border:1px solid #cbd9cf;border-radius:10px;box-shadow:0 12px 50px #20372e30;display:flex;flex-direction:column;overflow:hidden}.tasks-heading{display:flex;align-items:center;justify-content:space-between;padding:15px 18px;border-bottom:1px solid #e4ebe5}.tasks-note{font-size:12px;line-height:1.8;color:#718577;padding:12px 18px}.tasks-list{overflow:auto;overscroll-behavior:contain;flex:1;min-height:0}.task-card{padding:15px 18px;border-top:1px solid #e8eee9}.task-card h3{font-size:13px;margin:0 0 7px;overflow-wrap:anywhere;font-weight:550}.task-meta,.task-path,.task-stats{font-size:11px;color:#738a7b;line-height:1.7;overflow-wrap:anywhere}.task-path{font-family:ui-monospace,SFMono-Regular,monospace}.task-card progress{width:100%;height:6px;accent-color:#227b54;margin:10px 0 6px}.task-actions{display:flex;gap:7px;margin-top:10px}.task-actions button{font-size:12px;padding:5px 10px}.task-error,#tasks-connection{font-size:12px;color:#9f4b40;line-height:1.7;overflow-wrap:anywhere}.task-error{margin-top:8px}#tasks-connection{padding:0 18px 10px}#tasks-toggle{font-size:12px;background:#2f4b3e;border-color:#607969;color:#edf5ef}#tasks-count{font-variant-numeric:tabular-nums;margin-left:5px}@media(max-width:820px){.tasks-panel{top:140px;right:16px;bottom:16px}.upload-actions{gap:5px}.upload-actions button{font-size:12px;padding:7px 9px}}
22
49
  `;
23
50
  export const OSS_WEB_JS = String.raw `(() => {
24
51
  'use strict';
@@ -28,9 +55,31 @@ const supplied = fragment.get('token');
28
55
  let token = supplied || sessionStorage.getItem('numa-oss-web-token');
29
56
  if (supplied) { sessionStorage.setItem('numa-oss-web-token', supplied); history.replaceState(null, '', location.pathname); }
30
57
  const state = { id: 0, epoch: 0, dir: '', root: '', rows: [], marker: '', selected: new Set(), file: null,
31
- session: null, offset: 0, ready: false, loading: false, mutating: false, controller: null, previewController: null, xhr: null, cancelUpload: false, renewalBusy: false };
58
+ session: null, offset: 0, ready: false, loading: false, mutating: false, controller: null, previewController: null, renewalBusy: false, grantsLoading: false };
32
59
  const node = (tag, text, className) => { const el = document.createElement(tag); if (text !== undefined) el.textContent = text; if (className) el.className = className; return el; };
33
60
  function notice(text, success = false) { $('notice').textContent = text; $('notice').className = 'notice' + (success ? ' success' : ''); $('notice').hidden = !text; }
61
+ function setBusy(id, busy) {
62
+ const button = $(id); button.classList.toggle('is-loading', busy); button.setAttribute('aria-busy', String(busy));
63
+ }
64
+ function copyButton(value, label) {
65
+ const button = node('button', undefined, 'quiet copy-button'); button.title = '复制' + label; button.setAttribute('aria-label', button.title);
66
+ button.append($('copy-icon').content.cloneNode(true));
67
+ button.onclick = async () => {
68
+ try {
69
+ if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(value);
70
+ else {
71
+ const field = node('textarea', undefined, 'clipboard-fallback'); field.value = value; document.body.append(field); field.select();
72
+ try { if (!document.execCommand('copy')) throw new Error('copy unavailable'); } finally { field.remove(); button.focus(); }
73
+ }
74
+ button.classList.add('copied'); button.title = '已复制'; notice(label + '已复制。', true);
75
+ setTimeout(() => { button.classList.remove('copied'); button.title = '复制' + label; }, 1600);
76
+ } catch {
77
+ const field = node('input', undefined, 'copy-manual'); field.value = value; field.readOnly = true;
78
+ button.parentElement.append(field); field.select(); notice('浏览器未允许复制,请复制已选中的内容。');
79
+ }
80
+ };
81
+ return button;
82
+ }
34
83
  function fail(error) { if (error.name !== 'AbortError') notice(error.message || '操作失败,请重试。'); }
35
84
  function url(path, params = {}, id = state.id) { return '/api/' + path + '?' + new URLSearchParams({ grantId: String(id), ...params }); }
36
85
  async function api(path, params = {}, options = {}, id = state.id) {
@@ -47,11 +96,14 @@ function remaining() { return state.session ? Math.max(0, Math.ceil((Date.parse(
47
96
  function controls() {
48
97
  const active = state.ready && remaining() > 0 && !state.mutating;
49
98
  $('upload').disabled = !active || !state.session?.write;
99
+ $('upload-folder').disabled = $('upload').disabled;
50
100
  $('download').disabled = !active || !state.file || !state.session?.read;
51
101
  $('delete').disabled = !active || !state.file || !state.session?.delete;
52
102
  $('download-selected').disabled = !active || !state.selected.size || !state.session?.read;
53
- $('delete-selected').disabled = !active || !state.selected.size || !state.session?.delete;
103
+ $('delete-selected').disabled = !active || !state.selected.size || !state.session?.delete || state.rows.some(row => row.directory && state.selected.has(row.key));
54
104
  $('grant').disabled = state.mutating;
105
+ $('grants-refresh').disabled = state.mutating || state.grantsLoading;
106
+ $('refresh').disabled = state.mutating || state.loading || !state.session;
55
107
  $('renew').disabled = !state.id || state.renewalBusy;
56
108
  $('auto-renew').disabled = !state.session || state.renewalBusy;
57
109
  $('selection-count').textContent = state.selected.size ? '已选 ' + state.selected.size + ' 项' : '';
@@ -83,7 +135,7 @@ function renderRows() {
83
135
  for (const row of rows) {
84
136
  const tr = node('tr'); if (state.file?.key === row.key) tr.className = 'active';
85
137
  const checkCell = node('td', undefined, 'check-cell');
86
- if (!row.directory) { const check = document.createElement('input'); check.type = 'checkbox'; check.checked = state.selected.has(row.key); check.setAttribute('aria-label', '选择 ' + row.key); check.onchange = () => { if (check.checked) state.selected.add(row.key); else state.selected.delete(row.key); controls(); }; checkCell.append(check); }
138
+ { const check = document.createElement('input'); check.type = 'checkbox'; check.checked = state.selected.has(row.key); check.setAttribute('aria-label', '选择 ' + row.key); check.onchange = () => { if (check.checked) state.selected.add(row.key); else state.selected.delete(row.key); updateSelection(); controls(); }; checkCell.append(check); }
87
139
  tr.append(checkCell);
88
140
  const name = node('td'); const button = node('button', undefined, 'entry'); button.title = row.key;
89
141
  button.append(node('span', undefined, row.directory ? 'folder-icon' : 'file-icon'), node('span', row.key.slice(state.dir.length) || row.key));
@@ -94,15 +146,17 @@ function renderRows() {
94
146
  $('empty').hidden = rows.length > 0; $('empty').textContent = state.loading ? '正在读取文件…' : $('search').value ? '已加载列表中没有匹配项' : '此目录暂无文件';
95
147
  $('list-summary').textContent = '已加载 ' + state.rows.length + ' 项' + ($('search').value ? ' · 显示 ' + rows.length + ' 项' : '') + (state.marker ? ' · 还有更多文件' : '');
96
148
  $('more').hidden = !state.marker; $('more').disabled = state.loading;
97
- const files = rows.filter(r => !r.directory); $('select-all').checked = files.length > 0 && files.every(r => state.selected.has(r.key)); controls();
149
+ updateSelection(); controls();
98
150
  }
151
+ function updateSelection() { const rows = visible(), count = rows.filter(row => state.selected.has(row.key)).length; $('select-all').checked = rows.length > 0 && count === rows.length; $('select-all').indeterminate = count > 0 && count < rows.length; }
99
152
  function clearPreview() { state.previewController?.abort(); state.file = null; $('preview').replaceChildren(node('div', '选择文件以查看内容与信息', 'preview-empty')); controls(); }
100
153
  async function navigate(dir, append = false) {
154
+ if (state.mutating) return;
101
155
  if (state.session && remaining() <= 0) { notice('STS 已到期,请点击立即续期后继续。'); return; }
102
156
  const epoch = state.epoch, id = state.id; state.controller?.abort(); state.controller = new AbortController();
103
157
  const controller = state.controller;
104
158
  if (!append) { state.dir = dir; state.rows = []; state.selected.clear(); state.marker = ''; clearPreview(); }
105
- state.loading = true; renderRows(); breadcrumbs();
159
+ state.loading = true; setBusy('refresh', true); renderRows(); breadcrumbs();
106
160
  try {
107
161
  const data = await api('list', { prefix: dir, ...(append ? { marker: state.marker } : {}) }, { signal: controller.signal }, id);
108
162
  if (epoch !== state.epoch || controller !== state.controller) return;
@@ -112,7 +166,7 @@ async function navigate(dir, append = false) {
112
166
  state.rows = [...merged.values()].sort((a,b) => Number(b.directory) - Number(a.directory) || a.key.localeCompare(b.key));
113
167
  state.marker = data.listing.truncated ? data.listing.nextMarker || '' : '';
114
168
  } catch (error) { if (epoch === state.epoch && controller === state.controller) { fail(error); if (error.code?.includes('session') || error.code?.includes('refresh')) state.ready = false; } }
115
- finally { if (epoch === state.epoch && controller === state.controller) { state.loading = false; renderRows(); breadcrumbs(); } }
169
+ finally { if (epoch === state.epoch && controller === state.controller) { state.loading = false; setBusy('refresh', false); renderRows(); breadcrumbs(); } }
116
170
  }
117
171
  async function preview(row) {
118
172
  clearPreview(); state.file = row; renderRows();
@@ -123,21 +177,39 @@ async function preview(row) {
123
177
  if (epoch !== state.epoch || controller !== state.previewController) return;
124
178
  const box = $('preview'); box.replaceChildren(node('h3', row.key.split('/').pop()));
125
179
  if (data.kind === 'text') box.append(node('pre', data.text));
126
- else if (data.kind === 'image') { const img = document.createElement('img'); img.src = data.data; img.alt = row.key; box.append(img); }
180
+ else if (data.kind === 'image') { const img = document.createElement('img'); img.src = data.data; img.alt = row.key; img.onerror = () => img.replaceWith(node('p', '图片无法预览,请下载后查看。')); box.append(img); }
127
181
  else box.append(node('p', '此文件暂不支持在线预览,请下载后查看。支持文本与不超过 1 MiB 的常见图片。'));
128
182
  if (data.truncated && data.kind === 'text') box.append(node('p', '仅显示前 256 KiB;下载可查看完整内容。'));
129
- const dl = node('dl');
130
- for (const [label, value] of [['大小', bytes(data.metadata.contentLength)], ['类型', data.metadata.contentType || '—'], ['修改时间', date(data.metadata.lastModified)], ['对象路径', row.key]]) dl.append(node('dt', label), node('dd', value));
131
- box.append(dl);
183
+ const meta = data.metadata, dl = node('dl');
184
+ for (const [label, value] of [['大小', bytes(meta.contentLength)], ['类型', meta.contentType || '—'], ['修改时间', date(meta.lastModified)]]) dl.append(node('dt', label), node('dd', value));
185
+ const path = node('dd', undefined, 'copy-field'); path.append(copyButton(meta.key, '对象路径'), node('span', meta.key));
186
+ dl.append(node('dt', '对象路径'), path);
187
+ if (data.publicUrl) {
188
+ const address = node('dd', undefined, 'copy-field'), link = node('a', '打开链接');
189
+ link.href = data.publicUrl; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.title = '打开文件公网地址';
190
+ address.append(copyButton(data.publicUrl, '公网地址'), link); dl.append(node('dt', '公网地址'), address);
191
+ }
192
+ box.append(dl, node('p', '公网地址不含临时签名;私有对象需授权访问。'));
193
+ const standard = node('dl', undefined, 'metadata-list');
194
+ for (const [label, key] of [['ETag','etag'],['存储类型','storageClass'],['对象类型','objectType'],['版本 ID','versionId'],['CRC64','crc64'],['Cache-Control','cacheControl'],['Content-Disposition','contentDisposition'],['Content-Encoding','contentEncoding'],['Expires','expires'],['服务端加密','serverSideEncryption']]) {
195
+ if (meta[key] !== undefined) standard.append(node('dt', label), node('dd', String(meta[key])));
196
+ }
197
+ box.append(node('h4', 'OSS 系统元数据'), standard.children.length ? standard : node('p', '暂无其他系统元数据'));
198
+ const custom = node('dl', undefined, 'metadata-list');
199
+ for (const [key, value] of Object.entries(meta.metadata || {}).sort(([a], [b]) => a.localeCompare(b))) custom.append(node('dt', key.startsWith('x-oss-meta-') ? key : 'x-oss-meta-' + key), node('dd', value));
200
+ box.append(node('h4', 'OSS 自定义元数据'), custom.children.length ? custom : node('p', '暂无自定义元数据'));
132
201
  } catch (error) { if (epoch === state.epoch && controller === state.previewController && error.name !== 'AbortError') { $('preview').replaceChildren(node('h3', row.key.split('/').pop()), node('p', error.message)); } }
133
202
  }
134
203
  async function selectGrant(id) {
204
+ state.loading = false; setBusy('refresh', false);
135
205
  state.epoch++; const epoch = state.epoch; state.id = Number(id); state.ready = false; state.session = null; state.rows = []; state.selected.clear(); state.marker = ''; state.dir = ''; state.root = '';
136
206
  state.controller?.abort(); $('search').value = ''; clearPreview(); notice(''); tick(); renderRows(); $('scope').textContent = '正在连接…'; $('bucket').textContent = '正在连接…';
137
207
  try { const data = await api('session', {}, {}, state.id); if (epoch !== state.epoch) return; setSession(data.session); await navigate(state.root); }
138
208
  catch (error) { if (epoch === state.epoch) { fail(error); $('empty').textContent = '连接失败,请检查授权并重试。'; } }
139
209
  }
140
210
  async function refreshGrants(initial = false) {
211
+ if (state.grantsLoading) return;
212
+ state.grantsLoading = true; setBusy('grants-refresh', true); controls();
141
213
  try {
142
214
  const data = await api('grants'); const select = $('grant'); const previous = state.id;
143
215
  select.replaceChildren(); for (const grant of data.grants) { const option = node('option', (grant.policyCode || 'Grant') + ' · ' + grant.bucketName + '/' + grant.objectPrefix + ' (#' + grant.grantId + ')'); option.value = String(grant.grantId); select.append(option); }
@@ -145,6 +217,7 @@ async function refreshGrants(initial = false) {
145
217
  const id = data.grants.some(g => g.grantId === previous) ? previous : data.grants.some(g => g.grantId === data.selectedGrantId) ? data.selectedGrantId : data.grants[0].grantId;
146
218
  select.value = String(id); if (initial || id !== previous) await selectGrant(id);
147
219
  } catch (error) { fail(error); }
220
+ finally { state.grantsLoading = false; setBusy('grants-refresh', false); controls(); }
148
221
  }
149
222
  async function renew(manual = false) {
150
223
  if (!state.id || state.renewalBusy) return;
@@ -154,16 +227,77 @@ async function renew(manual = false) {
154
227
  catch (error) { if (epoch === state.epoch) { state.ready = false; fail(error); } }
155
228
  finally { state.renewalBusy = false; $('renew').textContent = '立即续期'; tick(); }
156
229
  }
230
+ async function chooseTransfer(path, body) {
231
+ if (state.mutating || !state.ready || remaining() <= 0) return;
232
+ const id = state.id; state.mutating = true; controls();
233
+ const controller = new AbortController(), timeout = setTimeout(() => controller.abort(), 5 * 60 * 1000);
234
+ notice('请在系统对话框中选择' + (path === 'transfers/download' ? '保存目录' : '上传内容') + '…');
235
+ try {
236
+ const data = await api(path, {}, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: controller.signal }, id);
237
+ if (data.cancelled) { notice('已取消选择。'); return; }
238
+ showTasks(true); notice('已加入本机传输队列。可在传输任务中查看进度。', true); await pollTransfers();
239
+ } catch (error) { if (error.name === 'AbortError') notice('选择或任务准备超时。请先查看传输任务,确认是否已加入队列。'); else fail(error); }
240
+ finally { clearTimeout(timeout); state.mutating = false; controls(); }
241
+ }
157
242
  async function download(keys) {
158
- const id = state.id;
159
- for (const key of keys) {
160
- try { const data = await api('download-ticket', { key }, { method: 'POST' }, id); const link = document.createElement('a'); link.href = data.path; link.download = key.split('/').pop(); document.body.append(link); link.click(); link.remove(); }
161
- catch (error) { fail(error); break; }
243
+ if (!state.session?.read || !keys.length) return;
244
+ const entries = keys.map(key => ({ key, directory: !!state.rows.find(row => row.key === key)?.directory }));
245
+ await chooseTransfer('transfers/download', { entries });
246
+ }
247
+ const taskCards = new Map(), taskStatuses = new Map(), taskBusy = new Set();
248
+ let pollingTransfers = false, pendingUploadNotice = false;
249
+ const taskLabels = { queued:'排队中', running:'传输中', pausing:'正在暂停', paused:'已暂停', cancelling:'正在取消', cancelled:'已取消', completed:'已完成', failed:'失败' };
250
+ function showTasks(show) { $('tasks-panel').hidden = !show; $('tasks-toggle').setAttribute('aria-expanded', String(show)); }
251
+ function setText(element, value) { if (element.textContent !== value) element.textContent = value; }
252
+ async function taskAction(id, action) {
253
+ if (taskBusy.has(id)) return;
254
+ taskBusy.add(id); const card = taskCards.get(id); if (card) for (const button of card.actions.values()) button.disabled = true;
255
+ try { await api('transfers/' + encodeURIComponent(id) + '/' + action, {}, { method:'POST' }); }
256
+ catch (error) { fail(error); }
257
+ finally { taskBusy.delete(id); await pollTransfers(); }
258
+ }
259
+ function renderTasks(jobs) {
260
+ const present = new Set(jobs.map(job => job.id));
261
+ for (const [id, card] of taskCards) if (!present.has(id)) { card.root.remove(); taskCards.delete(id); taskStatuses.delete(id); }
262
+ for (const job of jobs) {
263
+ let card = taskCards.get(job.id);
264
+ if (!card) {
265
+ const root = node('article', undefined, 'task-card'), title = node('h3'), meta = node('p', undefined, 'task-meta'), path = node('p', undefined, 'task-path'), progress = node('progress'), stats = node('p', undefined, 'task-stats'), error = node('p', undefined, 'task-error'), buttons = node('div', undefined, 'task-actions');
266
+ progress.max = 100; progress.setAttribute('aria-label', '文件传输进度');
267
+ const actions = new Map();
268
+ for (const [action, label] of [['pause','暂停'],['resume','恢复'],['cancel','取消']]) { const button = node('button', label); button.onclick = () => taskAction(job.id, action); actions.set(action, button); buttons.append(button); }
269
+ root.append(title, meta, path, progress, stats, error, buttons); $('tasks-list').append(root);
270
+ card = { root, title, meta, path, progress, stats, error, actions }; taskCards.set(job.id, card);
271
+ }
272
+ const prior = taskStatuses.get(job.id);
273
+ if (prior && prior !== 'completed' && job.status === 'completed' && job.direction === 'upload' && Number(job.grantId) === state.id) pendingUploadNotice = true;
274
+ taskStatuses.set(job.id, job.status);
275
+ setText(card.title, job.key); setText(card.meta, (job.direction === 'upload' ? '上传' : '下载') + ' · Grant #' + job.grantId + ' · ' + (taskLabels[job.status] || '状态未知'));
276
+ setText(card.path, job.localPath); card.path.title = job.localPath;
277
+ card.progress.value = job.size > 0 ? Math.min(100, job.transferred / job.size * 100) : job.status === 'completed' ? 100 : 0;
278
+ card.progress.setAttribute('aria-valuetext', bytes(job.transferred) + ' / ' + bytes(job.size));
279
+ setText(card.stats, bytes(job.transferred) + ' / ' + bytes(job.size) + (job.status === 'running' ? ' · ' + bytes(Math.max(0, Number(job.bytesPerSecond) || 0)) + '/s' : ''));
280
+ setText(card.error, job.error || ''); card.error.hidden = !job.error;
281
+ const available = { pause: ['queued','running'].includes(job.status), resume: ['paused','failed'].includes(job.status), cancel: ['queued','running','paused','failed'].includes(job.status) };
282
+ for (const [action, button] of card.actions) { button.hidden = !available[action]; button.disabled = taskBusy.has(job.id); }
162
283
  }
284
+ $('tasks-empty').hidden = jobs.length > 0;
285
+ setText($('tasks-count'), String(jobs.length));
286
+ $('tasks-toggle').title = jobs.filter(job => ['queued','running','pausing','cancelling'].includes(job.status)).length + ' 个进行中的任务';
287
+ if (pendingUploadNotice && !state.mutating && !state.loading) { pendingUploadNotice = false; notice('当前授权有上传任务已完成。点击刷新文件列表查看新文件;当前预览已保留。', true); }
288
+ }
289
+ async function pollTransfers() {
290
+ if (!token || pollingTransfers) return;
291
+ pollingTransfers = true;
292
+ try { const data = await api('transfers'); renderTasks(data.jobs || []); $('tasks-connection').hidden = true; }
293
+ catch (error) { setText($('tasks-connection'), error.message || '暂时无法读取任务状态。'); $('tasks-connection').hidden = false; }
294
+ finally { pollingTransfers = false; }
163
295
  }
164
296
  let deletion = null;
165
297
  function askDelete(keys) {
166
- if (!keys.length) return; deletion = { keys, id: state.id, dir: state.dir, epoch: state.epoch };
298
+ if (!keys.length || state.mutating || !state.session?.delete || remaining() <= 0) return;
299
+ if (keys.some(key => state.rows.find(row => row.key === key)?.directory)) { notice('目录仅支持下载。删除前请取消选择目录。'); return; }
300
+ deletion = { keys, id: state.id, dir: state.dir, epoch: state.epoch };
167
301
  $('delete-list').replaceChildren(...keys.map(key => node('li', key)));
168
302
  $('delete-target').textContent = 'Grant #' + state.id + ' · ' + state.session.bucket;
169
303
  $('delete-dialog').showModal();
@@ -174,32 +308,6 @@ async function deleteFiles() {
174
308
  catch (error) { notice('已删除 ' + done + ' 个文件,后续操作已停止。' + error.message); }
175
309
  finally { state.mutating = false; controls(); if (state.epoch === target.epoch) await navigate(target.dir); }
176
310
  }
177
- function sendFile(file, key, id, index, total) {
178
- return new Promise((resolve, reject) => {
179
- const xhr = new XMLHttpRequest(); state.xhr = xhr; xhr.open('PUT', url('object', { key }, id)); xhr.setRequestHeader('Authorization', 'Bearer ' + token); xhr.setRequestHeader('Content-Type', 'application/octet-stream');
180
- xhr.upload.onprogress = event => { if (event.lengthComputable) $('progress').value = event.loaded / event.total * 100; $('transfer-detail').textContent = (index + 1) + '/' + total + ' · ' + bytes(event.loaded) + ' / ' + bytes(file.size); };
181
- xhr.upload.onload = () => { $('transfer-detail').textContent = '上传数据已发送,正在等待 OSS 确认…'; };
182
- xhr.onload = () => { let data; try { data = JSON.parse(xhr.responseText); } catch {} if (xhr.status >= 200 && xhr.status < 300 && data?.ok) resolve(); else reject(new Error(data?.error?.message || '上传失败,请刷新目录检查结果。')); };
183
- xhr.onerror = () => reject(new Error('上传连接中断,请刷新目录检查结果。'));
184
- xhr.onabort = () => reject(new Error('上传已取消;若请求已提交,请刷新目录核对远程结果。'));
185
- xhr.send(file);
186
- });
187
- }
188
- async function upload(files) {
189
- if (!files.length) return;
190
- const id = state.id, dir = state.dir, epoch = state.epoch; state.mutating = true; state.cancelUpload = false; controls(); $('transfer').hidden = false; let done = 0;
191
- try {
192
- for (let i = 0; i < files.length; i++) {
193
- if (state.cancelUpload) break;
194
- const file = files[i]; if (file.size > 5 * 1024 ** 3) throw new Error('单文件最大支持 5 GiB。');
195
- if (!file.name.trim() || /[\\/]/.test(file.name)) throw new Error('文件名称无效。');
196
- $('transfer-title').textContent = '上传 ' + file.name; $('progress').value = 0;
197
- await sendFile(file, dir + file.name, id, i, files.length); done++;
198
- }
199
- notice('已上传 ' + done + ' 个文件。', true);
200
- } catch (error) { notice('已上传 ' + done + ' 个文件。' + error.message); }
201
- finally { state.xhr = null; state.mutating = false; $('transfer').hidden = true; $('file-input').value = ''; controls(); if (state.epoch === epoch) await navigate(dir); }
202
- }
203
311
  $('grant').onchange = () => selectGrant($('grant').value);
204
312
  $('grants-refresh').onclick = () => { if (!state.mutating) refreshGrants(); };
205
313
  $('renew').onclick = () => renew(true);
@@ -214,7 +322,7 @@ $('up').onclick = () => { const parts = state.dir.slice(state.root.length).split
214
322
  $('refresh').onclick = () => { notice(''); navigate(state.dir); };
215
323
  $('search').oninput = renderRows;
216
324
  $('more').onclick = () => navigate(state.dir, true);
217
- $('select-all').onchange = () => { for (const row of visible().filter(r => !r.directory)) { if ($('select-all').checked) state.selected.add(row.key); else state.selected.delete(row.key); } renderRows(); };
325
+ $('select-all').onchange = () => { for (const row of visible()) { if ($('select-all').checked) state.selected.add(row.key); else state.selected.delete(row.key); } renderRows(); };
218
326
  $('preview-close').onclick = () => { clearPreview(); renderRows(); };
219
327
  $('download').onclick = () => { if (state.file) download([state.file.key]); };
220
328
  $('download-selected').onclick = () => download([...state.selected]);
@@ -222,12 +330,14 @@ $('delete').onclick = () => { if (state.file) askDelete([state.file.key]); };
222
330
  $('delete-selected').onclick = () => askDelete([...state.selected]);
223
331
  $('delete-cancel').onclick = () => { deletion = null; $('delete-dialog').close(); };
224
332
  $('delete-confirm').onclick = deleteFiles;
225
- $('upload').onclick = () => $('file-input').click();
226
- $('file-input').onchange = () => upload([...$('file-input').files]);
227
- $('cancel-upload').onclick = () => { state.cancelUpload = true; state.xhr?.abort(); };
333
+ $('upload').onclick = () => { if (state.session?.write) chooseTransfer('transfers/upload', { prefix: state.dir, folder: false }); };
334
+ $('upload-folder').onclick = () => { if (state.session?.write) chooseTransfer('transfers/upload', { prefix: state.dir, folder: true }); };
335
+ $('tasks-toggle').onclick = () => showTasks($('tasks-panel').hidden);
336
+ $('tasks-close').onclick = () => { showTasks(false); $('tasks-toggle').focus(); };
337
+ setInterval(pollTransfers, 1000);
228
338
  setInterval(tick, 1000);
229
339
  setInterval(() => { if (state.id && !state.loading) renew(false); }, 15000);
230
340
  if (!token) { notice('请通过 numa oss web 启动,并使用 CLI 显示的完整链接打开页面。'); $('empty').textContent = '等待本机会话'; }
231
- else refreshGrants(true);
341
+ else { refreshGrants(true); pollTransfers(); }
232
342
  })();`;
233
343
  //# sourceMappingURL=web-page.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"web-page.js","sourceRoot":"","sources":["../../src/oss/web-page.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;eAgBvB,CAAC;AAEhB,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAA;;;CAGpC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAiN9B,CAAC"}
1
+ {"version":3,"file":"web-page.js","sourceRoot":"","sources":["../../src/oss/web-page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEjE,iGAAiG;AACjG,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;4PAIsN,gBAAgB;;;;;mOAKzC,gBAAgB;;;;;;;;2BAQxN,aAAa;eACzB,CAAC;AAEhB,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BpC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAoS9B,CAAC"}
package/dist/oss/web.d.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import { OssClient } from "./client.js";
2
2
  import { OssTransferClient } from "./transfers.js";
3
3
  import type { AccessGrant } from "./schemas.js";
4
+ import { type LocalPicker } from "./local-picker.js";
5
+ import type { TransferWorker } from "./transfer-types.js";
4
6
  export interface OssWebOptions {
5
7
  grantId: number;
6
8
  port?: number;
7
9
  openBrowser?: boolean;
8
10
  client?: Pick<OssClient, "listGrants" | "issueSession">;
9
11
  transfer?: OssTransferClient;
12
+ picker?: LocalPicker;
13
+ taskDirectory?: string;
14
+ taskNamespace?: string;
15
+ taskWorker?: TransferWorker;
10
16
  now?: () => number;
11
17
  }
12
18
  export interface OssWebHandle {