agim-cli 1.1.6 → 1.1.8

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.
@@ -4,6 +4,7 @@
4
4
  // base URL + an enable flag), and the defaults handle everything else. No
5
5
  // JSON-config schema dance, no on-disk state — change env, restart, done.
6
6
  import { DEFAULT_THRESHOLDS } from './render-router.js';
7
+ import { getCurrentTunnelUrl } from './tunnel.js';
7
8
  function parsePositiveInt(raw, fallback) {
8
9
  if (!raw)
9
10
  return fallback;
@@ -14,9 +15,12 @@ export function getViewerConfig() {
14
15
  const enabledRaw = (process.env.IMHUB_VIEWER_ENABLED || '').toLowerCase();
15
16
  const enabled = enabledRaw === '1' || enabledRaw === 'true' || enabledRaw === 'yes';
16
17
  const publicBaseUrl = (process.env.IMHUB_VIEWER_PUBLIC_BASE_URL || '').replace(/\/$/, '');
18
+ const tunnelModeRaw = (process.env.IMHUB_VIEWER_TUNNEL_MODE || 'off').toLowerCase();
19
+ const tunnelMode = tunnelModeRaw === 'quick' ? 'quick' : 'off';
17
20
  return {
18
21
  enabled,
19
22
  publicBaseUrl,
23
+ tunnelMode,
20
24
  thresholds: {
21
25
  chars: parsePositiveInt(process.env.IMHUB_VIEWER_CHARS, DEFAULT_THRESHOLDS.chars),
22
26
  lines: parsePositiveInt(process.env.IMHUB_VIEWER_LINES, DEFAULT_THRESHOLDS.lines),
@@ -24,14 +28,32 @@ export function getViewerConfig() {
24
28
  },
25
29
  };
26
30
  }
31
+ /**
32
+ * Resolve the effective public base URL. Order of preference:
33
+ * 1. Explicit IMHUB_VIEWER_PUBLIC_BASE_URL (operator-managed reverse proxy)
34
+ * 2. Auto-tunnel URL if tunnel_mode=quick and cloudflared has acquired a URL
35
+ * 3. Caller-supplied fallback
36
+ *
37
+ * Returns empty string when nothing usable is available.
38
+ */
39
+ export function getEffectivePublicBaseUrl(fallback) {
40
+ const cfg = getViewerConfig();
41
+ if (cfg.publicBaseUrl)
42
+ return cfg.publicBaseUrl;
43
+ if (cfg.tunnelMode === 'quick') {
44
+ const tu = getCurrentTunnelUrl();
45
+ if (tu)
46
+ return tu;
47
+ }
48
+ return (fallback || '').replace(/\/$/, '');
49
+ }
27
50
  /**
28
51
  * Build the URL placed in the IM reply for a saved paste. Returns null if
29
52
  * no public base URL is configured AND no fallback was provided. Caller
30
53
  * should treat null as "viewer can't produce a usable link — degrade".
31
54
  */
32
55
  export function buildPasteUrl(id, fallbackBaseUrl) {
33
- const cfg = getViewerConfig();
34
- const base = cfg.publicBaseUrl || fallbackBaseUrl || '';
56
+ const base = getEffectivePublicBaseUrl(fallbackBaseUrl);
35
57
  if (!base)
36
58
  return null;
37
59
  return `${base.replace(/\/$/, '')}/v/${id}`;
@@ -1 +1 @@
1
- {"version":3,"file":"viewer-config.js","sourceRoot":"","sources":["../../src/core/viewer-config.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,0EAA0E;AAE1E,OAAO,EAAE,kBAAkB,EAAyB,MAAM,oBAAoB,CAAA;AAgB9E,SAAS,gBAAgB,CAAC,GAAuB,EAAE,QAAgB;IACjE,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAA;IACzB,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAC3B,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;AACnD,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;IACzE,MAAM,OAAO,GAAG,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,MAAM,IAAI,UAAU,KAAK,KAAK,CAAA;IACnF,MAAM,aAAa,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IACzF,OAAO;QACL,OAAO;QACP,aAAa;QACb,UAAU,EAAE;YACV,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,KAAK,CAAC;YACjF,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,KAAK,CAAC;YACjF,SAAS,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,kBAAkB,CAAC,SAAS,CAAC;SAC/F;KACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,EAAU,EAAE,eAAwB;IAChE,MAAM,GAAG,GAAG,eAAe,EAAE,CAAA;IAC7B,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,IAAI,eAAe,IAAI,EAAE,CAAA;IACvD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAA;AAC7C,CAAC"}
1
+ {"version":3,"file":"viewer-config.js","sourceRoot":"","sources":["../../src/core/viewer-config.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,0EAA0E;AAE1E,OAAO,EAAE,kBAAkB,EAAyB,MAAM,oBAAoB,CAAA;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAyBjD,SAAS,gBAAgB,CAAC,GAAuB,EAAE,QAAgB;IACjE,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAA;IACzB,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAC3B,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;AACnD,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;IACzE,MAAM,OAAO,GAAG,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,MAAM,IAAI,UAAU,KAAK,KAAK,CAAA;IACnF,MAAM,aAAa,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IACzF,MAAM,aAAa,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAA;IACnF,MAAM,UAAU,GAAqB,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAA;IAChF,OAAO;QACL,OAAO;QACP,aAAa;QACb,UAAU;QACV,UAAU,EAAE;YACV,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,KAAK,CAAC;YACjF,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,KAAK,CAAC;YACjF,SAAS,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,kBAAkB,CAAC,SAAS,CAAC;SAC/F;KACF,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CAAC,QAAiB;IACzD,MAAM,GAAG,GAAG,eAAe,EAAE,CAAA;IAC7B,IAAI,GAAG,CAAC,aAAa;QAAE,OAAO,GAAG,CAAC,aAAa,CAAA;IAC/C,IAAI,GAAG,CAAC,UAAU,KAAK,OAAO,EAAE,CAAC;QAC/B,MAAM,EAAE,GAAG,mBAAmB,EAAE,CAAA;QAChC,IAAI,EAAE;YAAE,OAAO,EAAE,CAAA;IACnB,CAAC;IACD,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AAC5C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,EAAU,EAAE,eAAwB;IAChE,MAAM,IAAI,GAAG,yBAAyB,CAAC,eAAe,CAAC,CAAA;IACvD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAA;AAC7C,CAAC"}
@@ -168,6 +168,16 @@
168
168
  viewerSaved: 'Saved — change takes effect on the next reply.',
169
169
  viewerSaveFailed: 'Failed to save viewer settings',
170
170
  viewerMissingUrl: '⚠️ Public URL is empty — Agim will fall back to inline text for long replies (no link can be built).',
171
+ viewerTunnelMode: 'Auto-tunnel (cloudflared)',
172
+ viewerTunnelHint: 'For users without a public domain. Agim launches a cloudflared quick tunnel on startup and auto-detects a temporary `*.trycloudflare.com` URL. Requires the `cloudflared` binary to be installed (`brew install cloudflared` / `apt install cloudflared`). URL changes per restart — old paste links die. Leave off when you have a static reverse proxy.',
173
+ viewerTunnelOff: 'Off (use Public URL above)',
174
+ viewerTunnelQuick: 'Quick tunnel (temporary URL, no domain needed)',
175
+ viewerTunnelStatus: 'Tunnel status',
176
+ viewerTunnelRunning: 'Running',
177
+ viewerTunnelNotRunning: 'Not running',
178
+ viewerTunnelBinaryMissing: 'cloudflared not installed',
179
+ viewerTunnelCurrentUrl: 'Current URL',
180
+ viewerTunnelRefresh: 'Refresh',
171
181
  },
172
182
  zh: {
173
183
  title: 'Agim — 设置',
@@ -322,6 +332,16 @@
322
332
  viewerSaved: '已保存 — 下一条回复生效。',
323
333
  viewerSaveFailed: '保存 Viewer 配置失败',
324
334
  viewerMissingUrl: '⚠️ 公网域名为空 — 长回复将退回到内联文本(无法构造跳转链接)。',
335
+ viewerTunnelMode: '自动 tunnel(cloudflared)',
336
+ viewerTunnelHint: '给没有公网域名的用户。Agim 启动时自动拉起 cloudflared quick tunnel,拿一个临时 `*.trycloudflare.com` URL。需要先装 `cloudflared`(`brew install cloudflared` / `apt install cloudflared`)。**重启后 URL 会变,旧 paste 链接打不开**。有反代域名的话保持关闭即可。',
337
+ viewerTunnelOff: '关闭(用上面的公网域名)',
338
+ viewerTunnelQuick: 'Quick tunnel(临时 URL,无需域名)',
339
+ viewerTunnelStatus: 'Tunnel 状态',
340
+ viewerTunnelRunning: '运行中',
341
+ viewerTunnelNotRunning: '未运行',
342
+ viewerTunnelBinaryMissing: '未安装 cloudflared',
343
+ viewerTunnelCurrentUrl: '当前 URL',
344
+ viewerTunnelRefresh: '刷新',
325
345
  },
326
346
  };
327
347
  function t(key) { return T[window.__lang][key] || T.en[key] || key; }
@@ -1419,6 +1439,18 @@
1419
1439
  <input type="text" id="viewerPublicUrl" placeholder="https://agim.example.com" style="max-width:480px" />
1420
1440
  <p class="muted" style="margin-top:4px;font-size:12px">${t('viewerPublicUrlHint')}</p>
1421
1441
 
1442
+ <label style="margin-top:12px;display:block">${t('viewerTunnelMode')}</label>
1443
+ <select id="viewerTunnelMode" style="max-width:480px">
1444
+ <option value="off">${t('viewerTunnelOff')}</option>
1445
+ <option value="quick">${t('viewerTunnelQuick')}</option>
1446
+ </select>
1447
+ <p class="muted" style="margin-top:4px;font-size:12px">${t('viewerTunnelHint')}</p>
1448
+ <div id="viewerTunnelStatusBox" style="margin-top:6px;padding:8px 10px;border:1px solid var(--border, #d0d7de);border-radius:6px;font-size:12px;display:none">
1449
+ <div><strong>${t('viewerTunnelStatus')}:</strong> <span id="viewerTunnelState">—</span></div>
1450
+ <div style="margin-top:4px"><strong>${t('viewerTunnelCurrentUrl')}:</strong> <code id="viewerTunnelCurrentUrl" style="word-break:break-all">—</code></div>
1451
+ <button type="button" class="btn" id="viewerTunnelRefreshBtn" style="margin-top:6px;padding:4px 10px;font-size:12px">${t('viewerTunnelRefresh')}</button>
1452
+ </div>
1453
+
1422
1454
  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-top:12px;max-width:720px">
1423
1455
  <div>
1424
1456
  <label>${t('viewerChars')}</label>
@@ -1462,6 +1494,32 @@
1462
1494
  `;
1463
1495
  }
1464
1496
 
1497
+ async function refreshViewerTunnelStatus() {
1498
+ const stateEl = document.getElementById('viewerTunnelState');
1499
+ const urlEl = document.getElementById('viewerTunnelCurrentUrl');
1500
+ if (!stateEl || !urlEl) return;
1501
+ stateEl.textContent = '…';
1502
+ urlEl.textContent = '—';
1503
+ try {
1504
+ const data = await authFetch('/api/viewer/tunnel').then(r => r.json());
1505
+ const tun = data && data.tunnel ? data.tunnel : {};
1506
+ if (!tun.binaryFound) {
1507
+ stateEl.textContent = t('viewerTunnelBinaryMissing');
1508
+ stateEl.style.color = '#c0392b';
1509
+ } else if (tun.running) {
1510
+ stateEl.textContent = '✓ ' + t('viewerTunnelRunning');
1511
+ stateEl.style.color = '#16a34a';
1512
+ } else {
1513
+ stateEl.textContent = t('viewerTunnelNotRunning');
1514
+ stateEl.style.color = '';
1515
+ }
1516
+ urlEl.textContent = tun.url || (data && data.effectivePublicUrl) || '—';
1517
+ } catch (err) {
1518
+ stateEl.textContent = 'error';
1519
+ urlEl.textContent = (err && err.message) || String(err);
1520
+ }
1521
+ }
1522
+
1465
1523
  async function loadEnvSection(reveal) {
1466
1524
  try {
1467
1525
  const url = reveal ? '/api/env?reveal=1' : '/api/env';
@@ -1490,6 +1548,16 @@
1490
1548
  set('viewerLines', 'IMHUB_VIEWER_LINES');
1491
1549
  set('viewerCodeLines', 'IMHUB_VIEWER_CODE_LINES');
1492
1550
  set('viewerMaxPastes', 'IMHUB_VIEWER_MAX_PASTES');
1551
+ const vTunnelMode = document.getElementById('viewerTunnelMode');
1552
+ if (vTunnelMode) {
1553
+ vTunnelMode.value = (env['IMHUB_VIEWER_TUNNEL_MODE'] || 'off').toLowerCase() === 'quick' ? 'quick' : 'off';
1554
+ }
1555
+ // Show tunnel status panel only when tunnel mode = quick.
1556
+ const tunnelBox = document.getElementById('viewerTunnelStatusBox');
1557
+ if (tunnelBox) {
1558
+ tunnelBox.style.display = (vTunnelMode && vTunnelMode.value === 'quick') ? 'block' : 'none';
1559
+ if (vTunnelMode && vTunnelMode.value === 'quick') void refreshViewerTunnelStatus();
1560
+ }
1493
1561
  const viewerStatus = document.getElementById('viewerStatus');
1494
1562
  if (viewerStatus) {
1495
1563
  if (!env['IMHUB_VIEWER_PUBLIC_BASE_URL']) {
@@ -1801,6 +1869,13 @@
1801
1869
  });
1802
1870
  document.getElementById('revealBaidu')?.addEventListener('click', () => loadEnvSection(true));
1803
1871
 
1872
+ // Tunnel mode dropdown — show/hide status panel on change.
1873
+ document.getElementById('viewerTunnelMode')?.addEventListener('change', (e) => {
1874
+ const box = document.getElementById('viewerTunnelStatusBox');
1875
+ if (box) box.style.display = e.target.value === 'quick' ? 'block' : 'none';
1876
+ });
1877
+ document.getElementById('viewerTunnelRefreshBtn')?.addEventListener('click', () => refreshViewerTunnelStatus());
1878
+
1804
1879
  // Viewer card — save toggle + URL + thresholds in one round-trip.
1805
1880
  document.getElementById('saveViewer')?.addEventListener('click', async () => {
1806
1881
  const enabled = document.getElementById('viewerEnabled')?.checked ? '1' : '0';
@@ -1809,6 +1884,7 @@
1809
1884
  const lines = (document.getElementById('viewerLines')?.value || '').trim();
1810
1885
  const codeLines = (document.getElementById('viewerCodeLines')?.value || '').trim();
1811
1886
  const maxPastes = (document.getElementById('viewerMaxPastes')?.value || '').trim();
1887
+ const tunnelMode = document.getElementById('viewerTunnelMode')?.value || 'off';
1812
1888
  const status = document.getElementById('viewerStatus');
1813
1889
  try {
1814
1890
  await authFetch('/api/env', {
@@ -1822,6 +1898,7 @@
1822
1898
  IMHUB_VIEWER_LINES: lines || null,
1823
1899
  IMHUB_VIEWER_CODE_LINES: codeLines || null,
1824
1900
  IMHUB_VIEWER_MAX_PASTES: maxPastes || null,
1901
+ IMHUB_VIEWER_TUNNEL_MODE: tunnelMode || 'off',
1825
1902
  },
1826
1903
  }),
1827
1904
  });
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAsDA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA+rB/C"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAsDA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAksB/C"}
@@ -412,6 +412,9 @@ export async function startWebServer(options) {
412
412
  if (url.pathname === '/api/viewer' && req.method === 'GET') {
413
413
  return handleViewerList(req, res, url);
414
414
  }
415
+ if (url.pathname === '/api/viewer/tunnel' && req.method === 'GET') {
416
+ return handleViewerTunnelStatus(req, res);
417
+ }
415
418
  const viewerIdMatch = url.pathname.match(/^\/api\/viewer\/([0-9a-f-]{8,})$/i);
416
419
  if (viewerIdMatch && req.method === 'GET') {
417
420
  return handleViewerGet(req, res, viewerIdMatch[1]);
@@ -1640,6 +1643,7 @@ const ENV_EDITABLE_KEYS = [
1640
1643
  'IMHUB_VIEWER_LINES',
1641
1644
  'IMHUB_VIEWER_CODE_LINES',
1642
1645
  'IMHUB_VIEWER_MAX_PASTES',
1646
+ 'IMHUB_VIEWER_TUNNEL_MODE',
1643
1647
  ];
1644
1648
  const SECRET_KEYS = new Set(['IMHUB_SMTP_PASS', 'IMHUB_BAIDU_MAP_AK']);
1645
1649
  function maskSecret(v) {
@@ -2786,6 +2790,19 @@ async function handleViewerDelete(_req, res, id) {
2786
2790
  }
2787
2791
  sendJson(res, 200, { ok: true });
2788
2792
  }
2793
+ async function handleViewerTunnelStatus(_req, res) {
2794
+ const { getTunnelStatus } = await import('../core/tunnel.js');
2795
+ const { getViewerConfig, getEffectivePublicBaseUrl } = await import('../core/viewer-config.js');
2796
+ const cfg = getViewerConfig();
2797
+ const tunnel = getTunnelStatus();
2798
+ sendJson(res, 200, {
2799
+ enabled: cfg.enabled,
2800
+ tunnelMode: cfg.tunnelMode,
2801
+ staticPublicUrl: cfg.publicBaseUrl,
2802
+ effectivePublicUrl: getEffectivePublicBaseUrl(),
2803
+ tunnel,
2804
+ });
2805
+ }
2789
2806
  // ============================================
2790
2807
  // WebSocket chat handlers
2791
2808
  // ============================================