agim-cli 1.1.5 → 1.1.7
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/CHANGELOG.md +66 -0
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/core/message-sink.d.ts.map +1 -1
- package/dist/core/message-sink.js +48 -1
- package/dist/core/message-sink.js.map +1 -1
- package/dist/core/render-router.d.ts +49 -0
- package/dist/core/render-router.d.ts.map +1 -0
- package/dist/core/render-router.js +124 -0
- package/dist/core/render-router.js.map +1 -0
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +12 -1
- package/dist/core/router.js.map +1 -1
- package/dist/core/viewer-config.d.ts +22 -0
- package/dist/core/viewer-config.d.ts.map +1 -0
- package/dist/core/viewer-config.js +39 -0
- package/dist/core/viewer-config.js.map +1 -0
- package/dist/core/viewer-local.d.ts +34 -0
- package/dist/core/viewer-local.d.ts.map +1 -0
- package/dist/core/viewer-local.js +140 -0
- package/dist/core/viewer-local.js.map +1 -0
- package/dist/web/public/settings.html +128 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +92 -0
- package/dist/web/server.js.map +1 -1
- package/dist/web/viewer-render.d.ts +4 -0
- package/dist/web/viewer-render.d.ts.map +1 -0
- package/dist/web/viewer-render.js +120 -0
- package/dist/web/viewer-render.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type PasteContentType = 'markdown' | 'plain';
|
|
2
|
+
export interface PasteRow {
|
|
3
|
+
id: string;
|
|
4
|
+
content: string;
|
|
5
|
+
content_type: PasteContentType;
|
|
6
|
+
title: string | null;
|
|
7
|
+
source: string | null;
|
|
8
|
+
job_id: number | null;
|
|
9
|
+
created_at: number;
|
|
10
|
+
view_count: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SavePasteOpts {
|
|
13
|
+
content: string;
|
|
14
|
+
content_type?: PasteContentType;
|
|
15
|
+
title?: string;
|
|
16
|
+
source?: string;
|
|
17
|
+
job_id?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Save a paste. Returns the new row's id (uuidv4). Returns null if the
|
|
21
|
+
* underlying DB is broken (caller should fall back to truncation).
|
|
22
|
+
*/
|
|
23
|
+
export declare function savePaste(opts: SavePasteOpts): string | null;
|
|
24
|
+
export declare function getPaste(id: string): PasteRow | null;
|
|
25
|
+
export declare function bumpViewCount(id: string): void;
|
|
26
|
+
export interface ListOpts {
|
|
27
|
+
limit?: number;
|
|
28
|
+
offset?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function listPastes(opts?: ListOpts): PasteRow[];
|
|
31
|
+
export declare function deletePaste(id: string): boolean;
|
|
32
|
+
export declare function countPastes(): number;
|
|
33
|
+
export declare function closeViewerDb(): void;
|
|
34
|
+
//# sourceMappingURL=viewer-local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewer-local.d.ts","sourceRoot":"","sources":["../../src/core/viewer-local.ts"],"names":[],"mappings":"AA8CA,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,OAAO,CAAA;AAEnD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,gBAAgB,CAAA;IAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,gBAAgB,CAAA;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAWD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CA8C5D;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAQpD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAQ9C;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,wBAAgB,UAAU,CAAC,IAAI,GAAE,QAAa,GAAG,QAAQ,EAAE,CAS1D;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAK/C;AAED,wBAAgB,WAAW,IAAI,MAAM,CAKpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Viewer-local — local SQLite store for long IM-message payloads that get
|
|
2
|
+
// rendered as web pages via /v/:id on the agim web console.
|
|
3
|
+
//
|
|
4
|
+
// Why local: privacy — content stays on the operator's machine, full stop.
|
|
5
|
+
// IM messages carry only a short link to the operator's own publicly-reachable
|
|
6
|
+
// agim web (cloudflared / caddy / tailscale at operator's discretion). The
|
|
7
|
+
// link resolves to a row in this SQLite, rendered by the web console.
|
|
8
|
+
//
|
|
9
|
+
// Permanence: rows have NO TTL. They are kept until the operator hits the
|
|
10
|
+
// per-instance cap (`IMHUB_VIEWER_MAX_PASTES`, default 10_000), at which point
|
|
11
|
+
// the oldest rows are pruned LRU. Rationale: this is the operator's own data,
|
|
12
|
+
// equivalent to their memos / job history — they should keep it unless they
|
|
13
|
+
// explicitly delete.
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
import { logger as rootLogger } from './logger.js';
|
|
17
|
+
import { createSqliteHelper } from './sqlite-helper.js';
|
|
18
|
+
import { AGIM_HOME } from './agim-paths.js';
|
|
19
|
+
const VIEWER_DB = join(AGIM_HOME, 'viewer.db');
|
|
20
|
+
const SCHEMA = `
|
|
21
|
+
CREATE TABLE IF NOT EXISTS pastes (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
content TEXT NOT NULL,
|
|
24
|
+
content_type TEXT NOT NULL DEFAULT 'markdown',
|
|
25
|
+
title TEXT,
|
|
26
|
+
source TEXT,
|
|
27
|
+
job_id INTEGER,
|
|
28
|
+
created_at INTEGER NOT NULL,
|
|
29
|
+
view_count INTEGER NOT NULL DEFAULT 0
|
|
30
|
+
);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_pastes_created ON pastes(created_at DESC);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_pastes_job ON pastes(job_id);
|
|
33
|
+
`;
|
|
34
|
+
const viewerLog = rootLogger.child({ component: 'viewer-local' });
|
|
35
|
+
const helper = createSqliteHelper({
|
|
36
|
+
file: VIEWER_DB,
|
|
37
|
+
schema: SCHEMA,
|
|
38
|
+
logger: viewerLog,
|
|
39
|
+
component: 'viewer-local',
|
|
40
|
+
});
|
|
41
|
+
const DEFAULT_MAX_PASTES = 10_000;
|
|
42
|
+
function maxPastes() {
|
|
43
|
+
const raw = process.env.IMHUB_VIEWER_MAX_PASTES;
|
|
44
|
+
if (!raw)
|
|
45
|
+
return DEFAULT_MAX_PASTES;
|
|
46
|
+
const n = parseInt(raw, 10);
|
|
47
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAX_PASTES;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Save a paste. Returns the new row's id (uuidv4). Returns null if the
|
|
51
|
+
* underlying DB is broken (caller should fall back to truncation).
|
|
52
|
+
*/
|
|
53
|
+
export function savePaste(opts) {
|
|
54
|
+
const db = helper.get();
|
|
55
|
+
if (!db)
|
|
56
|
+
return null;
|
|
57
|
+
const id = randomUUID();
|
|
58
|
+
const now = Math.floor(Date.now() / 1000);
|
|
59
|
+
const ct = opts.content_type === 'plain' ? 'plain' : 'markdown';
|
|
60
|
+
try {
|
|
61
|
+
db.prepare(`
|
|
62
|
+
INSERT INTO pastes (id, content, content_type, title, source, job_id, created_at)
|
|
63
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
64
|
+
`).run(id, opts.content, ct, opts.title ?? null, opts.source ?? null, opts.job_id ?? null, now);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
viewerLog.warn({ event: 'viewer.save.failed', err: String(err) });
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
// LRU prune (cheap — only fires when over cap)
|
|
71
|
+
try {
|
|
72
|
+
const cap = maxPastes();
|
|
73
|
+
const total = db.prepare('SELECT COUNT(*) AS n FROM pastes').get();
|
|
74
|
+
if (total.n > cap) {
|
|
75
|
+
const over = total.n - cap;
|
|
76
|
+
const r = db.prepare(`
|
|
77
|
+
DELETE FROM pastes WHERE id IN (
|
|
78
|
+
SELECT id FROM pastes ORDER BY created_at ASC LIMIT ?
|
|
79
|
+
)
|
|
80
|
+
`).run(over);
|
|
81
|
+
if (r.changes > 0) {
|
|
82
|
+
viewerLog.info({ event: 'viewer.prune.lru', removed: r.changes, cap });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
viewerLog.warn({ event: 'viewer.prune.failed', err: String(err) });
|
|
88
|
+
}
|
|
89
|
+
return id;
|
|
90
|
+
}
|
|
91
|
+
export function getPaste(id) {
|
|
92
|
+
const db = helper.get();
|
|
93
|
+
if (!db)
|
|
94
|
+
return null;
|
|
95
|
+
const row = db.prepare(`
|
|
96
|
+
SELECT id, content, content_type, title, source, job_id, created_at, view_count
|
|
97
|
+
FROM pastes WHERE id = ?
|
|
98
|
+
`).get(id);
|
|
99
|
+
return row ?? null;
|
|
100
|
+
}
|
|
101
|
+
export function bumpViewCount(id) {
|
|
102
|
+
const db = helper.get();
|
|
103
|
+
if (!db)
|
|
104
|
+
return;
|
|
105
|
+
try {
|
|
106
|
+
db.prepare('UPDATE pastes SET view_count = view_count + 1 WHERE id = ?').run(id);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
viewerLog.warn({ event: 'viewer.bump.failed', err: String(err) });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function listPastes(opts = {}) {
|
|
113
|
+
const db = helper.get();
|
|
114
|
+
if (!db)
|
|
115
|
+
return [];
|
|
116
|
+
const limit = Math.min(Math.max(opts.limit ?? 50, 1), 500);
|
|
117
|
+
const offset = Math.max(opts.offset ?? 0, 0);
|
|
118
|
+
return db.prepare(`
|
|
119
|
+
SELECT id, content, content_type, title, source, job_id, created_at, view_count
|
|
120
|
+
FROM pastes ORDER BY created_at DESC LIMIT ? OFFSET ?
|
|
121
|
+
`).all(limit, offset);
|
|
122
|
+
}
|
|
123
|
+
export function deletePaste(id) {
|
|
124
|
+
const db = helper.get();
|
|
125
|
+
if (!db)
|
|
126
|
+
return false;
|
|
127
|
+
const r = db.prepare('DELETE FROM pastes WHERE id = ?').run(id);
|
|
128
|
+
return r.changes > 0;
|
|
129
|
+
}
|
|
130
|
+
export function countPastes() {
|
|
131
|
+
const db = helper.get();
|
|
132
|
+
if (!db)
|
|
133
|
+
return 0;
|
|
134
|
+
const row = db.prepare('SELECT COUNT(*) AS n FROM pastes').get();
|
|
135
|
+
return row.n;
|
|
136
|
+
}
|
|
137
|
+
export function closeViewerDb() {
|
|
138
|
+
helper.close();
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=viewer-local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewer-local.js","sourceRoot":"","sources":["../../src/core/viewer-local.ts"],"names":[],"mappings":"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"}
|
|
@@ -151,6 +151,23 @@
|
|
|
151
151
|
approvalPolicySave: 'Save',
|
|
152
152
|
approvalPolicySaved: 'Saved — applies to the next pending approval, no restart needed.',
|
|
153
153
|
approvalPolicyLoadFailed: 'Failed to load approval policy',
|
|
154
|
+
|
|
155
|
+
// ── Viewer (long-message web rendering) ────────────────────
|
|
156
|
+
viewerTitle: 'IM Long-Message Viewer',
|
|
157
|
+
viewerHint: 'When an agent reply is too long or contains a markdown table / large code block, Agim stores it locally in ~/.agim/viewer.db (permanent, never leaves this host) and sends the IM a short summary + a link. Set the public URL of your reverse-proxied Agim web here so the link is reachable from your phone.',
|
|
158
|
+
viewerEnabled: 'Enabled',
|
|
159
|
+
viewerDisabled: 'Disabled',
|
|
160
|
+
viewerEnabledLabel: 'Viewer mode',
|
|
161
|
+
viewerPublicUrl: 'Public base URL',
|
|
162
|
+
viewerPublicUrlHint: 'Your public URL pointing at this Agim web (e.g. https://agim.example.com). Use cloudflared / caddy / tailscale to expose port 3000.',
|
|
163
|
+
viewerChars: 'Char threshold',
|
|
164
|
+
viewerLines: 'Line threshold',
|
|
165
|
+
viewerCodeLines: 'Code-block line threshold',
|
|
166
|
+
viewerMaxPastes: 'Max stored pastes (LRU prune)',
|
|
167
|
+
viewerSave: 'Save',
|
|
168
|
+
viewerSaved: 'Saved — change takes effect on the next reply.',
|
|
169
|
+
viewerSaveFailed: 'Failed to save viewer settings',
|
|
170
|
+
viewerMissingUrl: '⚠️ Public URL is empty — Agim will fall back to inline text for long replies (no link can be built).',
|
|
154
171
|
},
|
|
155
172
|
zh: {
|
|
156
173
|
title: 'Agim — 设置',
|
|
@@ -288,6 +305,23 @@
|
|
|
288
305
|
approvalPolicySave: '保存',
|
|
289
306
|
approvalPolicySaved: '已保存 — 下一个挂起的审批起就生效,无需重启。',
|
|
290
307
|
approvalPolicyLoadFailed: '加载审批策略失败',
|
|
308
|
+
|
|
309
|
+
// ── Viewer (长内容 web 渲染) ─────────────────────────────
|
|
310
|
+
viewerTitle: 'IM 长内容 Viewer',
|
|
311
|
+
viewerHint: 'Agent 回答过长或含 markdown 表格 / 大段代码时,Agim 把全文存到本机 ~/.agim/viewer.db(永久保存,绝不离开本机),IM 里只发短摘要 + 跳转链接。这里填你反代到本机 Agim web 的公网域名,链接才能让手机/微信打开。',
|
|
312
|
+
viewerEnabled: '开启',
|
|
313
|
+
viewerDisabled: '关闭',
|
|
314
|
+
viewerEnabledLabel: 'Viewer 状态',
|
|
315
|
+
viewerPublicUrl: '公网域名',
|
|
316
|
+
viewerPublicUrlHint: '指向本机 Agim web 的公网域名(例如 https://agim.example.com)。用 cloudflared / caddy / tailscale 把 3000 端口暴露出去即可。',
|
|
317
|
+
viewerChars: '字符阈值',
|
|
318
|
+
viewerLines: '行数阈值',
|
|
319
|
+
viewerCodeLines: '代码块行数阈值',
|
|
320
|
+
viewerMaxPastes: '本机最多保留条数(LRU 淘汰)',
|
|
321
|
+
viewerSave: '保存',
|
|
322
|
+
viewerSaved: '已保存 — 下一条回复生效。',
|
|
323
|
+
viewerSaveFailed: '保存 Viewer 配置失败',
|
|
324
|
+
viewerMissingUrl: '⚠️ 公网域名为空 — 长回复将退回到内联文本(无法构造跳转链接)。',
|
|
291
325
|
},
|
|
292
326
|
};
|
|
293
327
|
function t(key) { return T[window.__lang][key] || T.en[key] || key; }
|
|
@@ -1369,6 +1403,47 @@
|
|
|
1369
1403
|
<p class="muted" id="smtpStatus" style="margin-top:8px;font-size:12px"></p>
|
|
1370
1404
|
</div>
|
|
1371
1405
|
|
|
1406
|
+
<!-- Viewer (env-file) — long-message web rendering -->
|
|
1407
|
+
<div class="card">
|
|
1408
|
+
<h2>📄 ${t('viewerTitle')}</h2>
|
|
1409
|
+
<p class="muted" style="margin-top:-4px;margin-bottom:12px;font-size:13px">${t('viewerHint')}</p>
|
|
1410
|
+
|
|
1411
|
+
<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:center;margin-bottom:12px">
|
|
1412
|
+
<label style="display:flex;gap:8px;align-items:center;font-weight:600">
|
|
1413
|
+
<input type="checkbox" id="viewerEnabled" />
|
|
1414
|
+
${t('viewerEnabledLabel')}
|
|
1415
|
+
</label>
|
|
1416
|
+
</div>
|
|
1417
|
+
|
|
1418
|
+
<label>${t('viewerPublicUrl')}</label>
|
|
1419
|
+
<input type="text" id="viewerPublicUrl" placeholder="https://agim.example.com" style="max-width:480px" />
|
|
1420
|
+
<p class="muted" style="margin-top:4px;font-size:12px">${t('viewerPublicUrlHint')}</p>
|
|
1421
|
+
|
|
1422
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-top:12px;max-width:720px">
|
|
1423
|
+
<div>
|
|
1424
|
+
<label>${t('viewerChars')}</label>
|
|
1425
|
+
<input type="number" id="viewerChars" min="100" max="10000" placeholder="500" />
|
|
1426
|
+
</div>
|
|
1427
|
+
<div>
|
|
1428
|
+
<label>${t('viewerLines')}</label>
|
|
1429
|
+
<input type="number" id="viewerLines" min="5" max="200" placeholder="12" />
|
|
1430
|
+
</div>
|
|
1431
|
+
<div>
|
|
1432
|
+
<label>${t('viewerCodeLines')}</label>
|
|
1433
|
+
<input type="number" id="viewerCodeLines" min="5" max="200" placeholder="10" />
|
|
1434
|
+
</div>
|
|
1435
|
+
<div>
|
|
1436
|
+
<label>${t('viewerMaxPastes')}</label>
|
|
1437
|
+
<input type="number" id="viewerMaxPastes" min="100" max="1000000" placeholder="10000" />
|
|
1438
|
+
</div>
|
|
1439
|
+
</div>
|
|
1440
|
+
|
|
1441
|
+
<div class="actions" style="margin-top:12px">
|
|
1442
|
+
<button type="button" class="btn btn-primary" id="saveViewer">${t('viewerSave')}</button>
|
|
1443
|
+
</div>
|
|
1444
|
+
<p class="muted" id="viewerStatus" style="margin-top:8px;font-size:12px"></p>
|
|
1445
|
+
</div>
|
|
1446
|
+
|
|
1372
1447
|
<!-- Baidu Maps AK (env-file) — feeds /memo address geocoding -->
|
|
1373
1448
|
<div class="card">
|
|
1374
1449
|
<h2>🗺 Baidu Maps AK</h2>
|
|
@@ -1404,6 +1479,28 @@
|
|
|
1404
1479
|
const sec = document.getElementById('smtpSecure');
|
|
1405
1480
|
if (sec) sec.value = env['IMHUB_SMTP_SECURE'] || 'auto';
|
|
1406
1481
|
set('baiduAk', 'IMHUB_BAIDU_MAP_AK');
|
|
1482
|
+
// Viewer card fields
|
|
1483
|
+
const vEnabled = document.getElementById('viewerEnabled');
|
|
1484
|
+
if (vEnabled) {
|
|
1485
|
+
const v = (env['IMHUB_VIEWER_ENABLED'] || '').toLowerCase();
|
|
1486
|
+
vEnabled.checked = v === '1' || v === 'true' || v === 'yes';
|
|
1487
|
+
}
|
|
1488
|
+
set('viewerPublicUrl', 'IMHUB_VIEWER_PUBLIC_BASE_URL');
|
|
1489
|
+
set('viewerChars', 'IMHUB_VIEWER_CHARS');
|
|
1490
|
+
set('viewerLines', 'IMHUB_VIEWER_LINES');
|
|
1491
|
+
set('viewerCodeLines', 'IMHUB_VIEWER_CODE_LINES');
|
|
1492
|
+
set('viewerMaxPastes', 'IMHUB_VIEWER_MAX_PASTES');
|
|
1493
|
+
const viewerStatus = document.getElementById('viewerStatus');
|
|
1494
|
+
if (viewerStatus) {
|
|
1495
|
+
if (!env['IMHUB_VIEWER_PUBLIC_BASE_URL']) {
|
|
1496
|
+
viewerStatus.textContent = t('viewerMissingUrl');
|
|
1497
|
+
} else if ((env['IMHUB_VIEWER_ENABLED'] || '').toLowerCase() === '1' ||
|
|
1498
|
+
(env['IMHUB_VIEWER_ENABLED'] || '').toLowerCase() === 'true') {
|
|
1499
|
+
viewerStatus.textContent = `✓ ${t('viewerEnabled')} → ${env['IMHUB_VIEWER_PUBLIC_BASE_URL']}`;
|
|
1500
|
+
} else {
|
|
1501
|
+
viewerStatus.textContent = `${t('viewerDisabled')} (${env['IMHUB_VIEWER_PUBLIC_BASE_URL']})`;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1407
1504
|
const smtpStatus = document.getElementById('smtpStatus');
|
|
1408
1505
|
const baiduStatus = document.getElementById('baiduStatus');
|
|
1409
1506
|
if (smtpStatus) smtpStatus.textContent = env['IMHUB_SMTP_HOST']
|
|
@@ -1704,6 +1801,37 @@
|
|
|
1704
1801
|
});
|
|
1705
1802
|
document.getElementById('revealBaidu')?.addEventListener('click', () => loadEnvSection(true));
|
|
1706
1803
|
|
|
1804
|
+
// Viewer card — save toggle + URL + thresholds in one round-trip.
|
|
1805
|
+
document.getElementById('saveViewer')?.addEventListener('click', async () => {
|
|
1806
|
+
const enabled = document.getElementById('viewerEnabled')?.checked ? '1' : '0';
|
|
1807
|
+
const url = (document.getElementById('viewerPublicUrl')?.value || '').trim();
|
|
1808
|
+
const chars = (document.getElementById('viewerChars')?.value || '').trim();
|
|
1809
|
+
const lines = (document.getElementById('viewerLines')?.value || '').trim();
|
|
1810
|
+
const codeLines = (document.getElementById('viewerCodeLines')?.value || '').trim();
|
|
1811
|
+
const maxPastes = (document.getElementById('viewerMaxPastes')?.value || '').trim();
|
|
1812
|
+
const status = document.getElementById('viewerStatus');
|
|
1813
|
+
try {
|
|
1814
|
+
await authFetch('/api/env', {
|
|
1815
|
+
method: 'PUT',
|
|
1816
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1817
|
+
body: JSON.stringify({
|
|
1818
|
+
updates: {
|
|
1819
|
+
IMHUB_VIEWER_ENABLED: enabled,
|
|
1820
|
+
IMHUB_VIEWER_PUBLIC_BASE_URL: url || null,
|
|
1821
|
+
IMHUB_VIEWER_CHARS: chars || null,
|
|
1822
|
+
IMHUB_VIEWER_LINES: lines || null,
|
|
1823
|
+
IMHUB_VIEWER_CODE_LINES: codeLines || null,
|
|
1824
|
+
IMHUB_VIEWER_MAX_PASTES: maxPastes || null,
|
|
1825
|
+
},
|
|
1826
|
+
}),
|
|
1827
|
+
});
|
|
1828
|
+
if (status) status.textContent = t('viewerSaved');
|
|
1829
|
+
await loadEnvSection();
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
if (status) status.textContent = t('viewerSaveFailed') + ': ' + (err && err.message ? err.message : err);
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1707
1835
|
// ==========================================
|
|
1708
1836
|
// Workspaces (PR-C)
|
|
1709
1837
|
// ==========================================
|
package/dist/web/server.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/web/server.js
CHANGED
|
@@ -106,6 +106,15 @@ export async function startWebServer(options) {
|
|
|
106
106
|
if (url.pathname === '/loc' && req.method === 'GET') {
|
|
107
107
|
return serveStatic(res, join(PUBLIC_DIR, 'loc.html'), 'text/html; charset=utf-8');
|
|
108
108
|
}
|
|
109
|
+
// v1.2 — Viewer: rendered markdown for long IM messages. URL is
|
|
110
|
+
// unguessable uuidv4; same trust model as /loc (public-on-purpose,
|
|
111
|
+
// operator fronts the web with their reverse proxy / tunnel).
|
|
112
|
+
// Content stays in ~/.agim/viewer.db on this host — the public URL
|
|
113
|
+
// is just a proxy back to here. See src/core/viewer-local.ts.
|
|
114
|
+
const viewerMatch = url.pathname.match(/^\/v\/([0-9a-f-]{8,})$/i);
|
|
115
|
+
if (viewerMatch && req.method === 'GET') {
|
|
116
|
+
return handleViewerPage(res, viewerMatch[1]);
|
|
117
|
+
}
|
|
109
118
|
// Short alias: /l/<token> serves the same H5 page. The page auto-detects
|
|
110
119
|
// either ?t= query or /l/<token> path. Lets us issue ~38-char URLs
|
|
111
120
|
// (vs 70+ for /loc?t=<32-hex>) which keeps WeChat chat cleaner.
|
|
@@ -396,6 +405,20 @@ export async function startWebServer(options) {
|
|
|
396
405
|
if (artifactsFileMatch && req.method === 'GET') {
|
|
397
406
|
return handleArtifactsFile(req, res, parseInt(artifactsFileMatch[1], 10), artifactsFileMatch[2]);
|
|
398
407
|
}
|
|
408
|
+
// v1.2 — Viewer pastes (long markdown stash for IM short-link replies).
|
|
409
|
+
// GET /api/viewer — list pastes (newest first), for the web console.
|
|
410
|
+
// GET /api/viewer/:id — raw JSON of one paste.
|
|
411
|
+
// DELETE /api/viewer/:id — remove a paste (operator can clean up).
|
|
412
|
+
if (url.pathname === '/api/viewer' && req.method === 'GET') {
|
|
413
|
+
return handleViewerList(req, res, url);
|
|
414
|
+
}
|
|
415
|
+
const viewerIdMatch = url.pathname.match(/^\/api\/viewer\/([0-9a-f-]{8,})$/i);
|
|
416
|
+
if (viewerIdMatch && req.method === 'GET') {
|
|
417
|
+
return handleViewerGet(req, res, viewerIdMatch[1]);
|
|
418
|
+
}
|
|
419
|
+
if (viewerIdMatch && req.method === 'DELETE') {
|
|
420
|
+
return handleViewerDelete(req, res, viewerIdMatch[1]);
|
|
421
|
+
}
|
|
399
422
|
// PR-B: agent health snapshot (circuit breaker + rate-limiter remaining
|
|
400
423
|
// + latency p50/95/99) consumed by the Health tab in /tasks.
|
|
401
424
|
if (url.pathname === '/api/agent-health' && req.method === 'GET') {
|
|
@@ -1608,6 +1631,15 @@ const ENV_EDITABLE_KEYS = [
|
|
|
1608
1631
|
// Hot-reload: handlePutEnv mutates process.env so approval-bus picks up the
|
|
1609
1632
|
// new value on the next timer fire, no restart needed.
|
|
1610
1633
|
'IMHUB_TIMEOUT_DEFAULT',
|
|
1634
|
+
// v1.2 — IM long-message viewer. Operator sets the public URL pointing at
|
|
1635
|
+
// their reverse-proxied agim web port; thresholds tune when routing kicks
|
|
1636
|
+
// in. Hot-reload works because viewer-config reads process.env every call.
|
|
1637
|
+
'IMHUB_VIEWER_ENABLED',
|
|
1638
|
+
'IMHUB_VIEWER_PUBLIC_BASE_URL',
|
|
1639
|
+
'IMHUB_VIEWER_CHARS',
|
|
1640
|
+
'IMHUB_VIEWER_LINES',
|
|
1641
|
+
'IMHUB_VIEWER_CODE_LINES',
|
|
1642
|
+
'IMHUB_VIEWER_MAX_PASTES',
|
|
1611
1643
|
];
|
|
1612
1644
|
const SECRET_KEYS = new Set(['IMHUB_SMTP_PASS', 'IMHUB_BAIDU_MAP_AK']);
|
|
1613
1645
|
function maskSecret(v) {
|
|
@@ -2694,6 +2726,66 @@ async function handleArtifactsFile(_req, res, jobId, name) {
|
|
|
2694
2726
|
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2695
2727
|
}
|
|
2696
2728
|
}
|
|
2729
|
+
// ─── Viewer handlers ───
|
|
2730
|
+
// v1.2 — Render a stored paste as an HTML page. URL is unguessable uuidv4
|
|
2731
|
+
// (32 hex chars including dashes); we still cap the length match to a
|
|
2732
|
+
// reasonable max in the router to avoid pathological lookups.
|
|
2733
|
+
async function handleViewerPage(res, id) {
|
|
2734
|
+
const { getPaste, bumpViewCount } = await import('../core/viewer-local.js');
|
|
2735
|
+
const { renderPasteHtml, renderNotFoundHtml } = await import('./viewer-render.js');
|
|
2736
|
+
const row = getPaste(id);
|
|
2737
|
+
if (!row) {
|
|
2738
|
+
res.writeHead(404, {
|
|
2739
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
2740
|
+
'X-Robots-Tag': 'noindex, nofollow',
|
|
2741
|
+
});
|
|
2742
|
+
res.end(renderNotFoundHtml());
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
bumpViewCount(id);
|
|
2746
|
+
res.writeHead(200, {
|
|
2747
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
2748
|
+
'X-Robots-Tag': 'noindex, nofollow',
|
|
2749
|
+
'Cache-Control': 'no-store',
|
|
2750
|
+
});
|
|
2751
|
+
res.end(renderPasteHtml(row));
|
|
2752
|
+
}
|
|
2753
|
+
async function handleViewerList(_req, res, url) {
|
|
2754
|
+
const { listPastes, countPastes } = await import('../core/viewer-local.js');
|
|
2755
|
+
const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') || '50', 10) || 50, 1), 500);
|
|
2756
|
+
const offset = Math.max(parseInt(url.searchParams.get('offset') || '0', 10) || 0, 0);
|
|
2757
|
+
const rows = listPastes({ limit, offset });
|
|
2758
|
+
// Strip content from list response to keep payload light; UI loads full row via GET /api/viewer/:id.
|
|
2759
|
+
const items = rows.map((r) => ({
|
|
2760
|
+
id: r.id,
|
|
2761
|
+
content_type: r.content_type,
|
|
2762
|
+
title: r.title,
|
|
2763
|
+
source: r.source,
|
|
2764
|
+
job_id: r.job_id,
|
|
2765
|
+
created_at: r.created_at,
|
|
2766
|
+
view_count: r.view_count,
|
|
2767
|
+
bytes: Buffer.byteLength(r.content, 'utf8'),
|
|
2768
|
+
}));
|
|
2769
|
+
sendJson(res, 200, { total: countPastes(), limit, offset, items });
|
|
2770
|
+
}
|
|
2771
|
+
async function handleViewerGet(_req, res, id) {
|
|
2772
|
+
const { getPaste } = await import('../core/viewer-local.js');
|
|
2773
|
+
const row = getPaste(id);
|
|
2774
|
+
if (!row) {
|
|
2775
|
+
sendJson(res, 404, { error: 'not_found' });
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
sendJson(res, 200, row);
|
|
2779
|
+
}
|
|
2780
|
+
async function handleViewerDelete(_req, res, id) {
|
|
2781
|
+
const { deletePaste } = await import('../core/viewer-local.js');
|
|
2782
|
+
const ok = deletePaste(id);
|
|
2783
|
+
if (!ok) {
|
|
2784
|
+
sendJson(res, 404, { error: 'not_found' });
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
sendJson(res, 200, { ok: true });
|
|
2788
|
+
}
|
|
2697
2789
|
// ============================================
|
|
2698
2790
|
// WebSocket chat handlers
|
|
2699
2791
|
// ============================================
|