@wendongfly/myhi 1.3.202 → 1.3.204
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/bin/daemon.js +28 -19
- package/bin/install-check.js +59 -3
- package/dist/admin.html.gz +0 -0
- package/dist/chat.html +105 -5
- package/dist/chat.html.gz +0 -0
- package/dist/grid.html.gz +0 -0
- package/dist/index.html +34 -10
- package/dist/index.html.gz +0 -0
- package/dist/index.js +6 -6
- package/dist/index.min.js +174 -160
- package/dist/lib/attn.js +6 -4
- package/dist/login.html.gz +0 -0
- package/dist/terminal.html.gz +0 -0
- package/package.json +1 -1
package/bin/daemon.js
CHANGED
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'url';
|
|
|
9
9
|
import { dirname, join } from 'path';
|
|
10
10
|
import { writeFileSync, readFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
11
11
|
import { homedir } from 'os';
|
|
12
|
-
import { checkInstall, upgradeStatusFile, npmUpgradeCmd } from './install-check.js';
|
|
12
|
+
import { checkInstall, upgradeStatusFile, npmUpgradeCmd, REGISTRIES, registryName } from './install-check.js';
|
|
13
13
|
|
|
14
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
15
|
const __dirname = dirname(__filename);
|
|
@@ -75,7 +75,7 @@ function startServer() {
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
child.on('message', (msg) => {
|
|
78
|
-
if (msg?.type === 'upgrade') doUpgrade(msg.target);
|
|
78
|
+
if (msg?.type === 'upgrade') doUpgrade(msg.target, msg.candidates);
|
|
79
79
|
else if (msg?.type === 'restart') doRestart();
|
|
80
80
|
});
|
|
81
81
|
|
|
@@ -123,31 +123,40 @@ function doRestart() {
|
|
|
123
123
|
// 停服升级:先让 server 退出(释放 node-pty 的 pty.node,Windows 上它被占用时 npm 覆盖会 EBUSY、
|
|
124
124
|
// 装出半成品),再 npm install,装完校验文件齐全 + 版本对,最后拉起 server。
|
|
125
125
|
// 不论成败都拉起 server(失败时 npm 一般已回滚,旧版照常跑);结果写 upgrade-status.json 给网页看。
|
|
126
|
-
|
|
126
|
+
// candidates:server 算好的下载源顺序 [{registry, version}](官方 → 国内镜像),装失败就换下一个
|
|
127
|
+
function doUpgrade(target, candidates) {
|
|
127
128
|
if (upgrading) { log('升级已在进行中,忽略'); return; }
|
|
128
129
|
upgrading = true;
|
|
129
130
|
const from = (() => { try { return JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')).version; } catch { return null; } })();
|
|
130
131
|
log(`收到升级请求(${from} → ${target || 'latest'}),先停 server 释放文件占用...`);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
const list = Array.isArray(candidates) && candidates.length ? candidates : [{ registry: REGISTRIES.official, version: target }];
|
|
133
|
+
const fails = [];
|
|
134
|
+
stopChild(() => tryNext(0));
|
|
135
|
+
function tryNext(i) {
|
|
136
|
+
if (i >= list.length) return finish({ ok: false, from, to: target || null, error: fails.join('\n\n') });
|
|
137
|
+
const c = list[i];
|
|
138
|
+
const cmd = npmUpgradeCmd(pkgRoot, c.registry); // 同一套 Node 的 npm + 装回 myhi 当前目录(见 install-check.js)
|
|
139
|
+
log(`从${registryName(c.registry)}安装,执行: ` + cmd);
|
|
134
140
|
exec(cmd, { timeout: 600000, encoding: 'utf8', windowsHide: true }, (err, stdout, stderr) => {
|
|
135
|
-
let result;
|
|
136
141
|
if (err) {
|
|
137
|
-
const tail = String(stderr || stdout || err.message).split('\n').filter((l) => l.trim()).slice(-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const chk = checkInstall(pkgRoot, target);
|
|
141
|
-
result = chk.ok ? { ok: true, from, to: chk.version } : { ok: false, from, to: target || null, error: chk.error };
|
|
142
|
+
const tail = String(stderr || stdout || err.message).split('\n').filter((l) => l.trim()).slice(-4).join('\n');
|
|
143
|
+
fails.push(`${registryName(c.registry)}:npm install 失败\n${tail}`);
|
|
144
|
+
return tryNext(i + 1);
|
|
142
145
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
upgrading = false;
|
|
147
|
-
crashCount = 0;
|
|
148
|
-
startServer();
|
|
146
|
+
const chk = checkInstall(pkgRoot, c.version || target);
|
|
147
|
+
if (!chk.ok) { fails.push(`${registryName(c.registry)}:${chk.error}`); return tryNext(i + 1); }
|
|
148
|
+
finish({ ok: true, from, to: chk.version });
|
|
149
149
|
});
|
|
150
|
-
}
|
|
150
|
+
}
|
|
151
|
+
// 不论成败都拉起 server(失败时 npm 一般已回滚,旧版照常跑);结果写 upgrade-status.json 给网页看
|
|
152
|
+
function finish(result) {
|
|
153
|
+
result.at = Date.now();
|
|
154
|
+
log(result.ok ? `升级成功:${from} → ${result.to}` : `升级失败:${result.error}`);
|
|
155
|
+
try { writeFileSync(upgradeStatusFile(configDir), JSON.stringify(result, null, 2)); } catch {}
|
|
156
|
+
upgrading = false;
|
|
157
|
+
crashCount = 0;
|
|
158
|
+
startServer();
|
|
159
|
+
}
|
|
151
160
|
}
|
|
152
161
|
|
|
153
162
|
function cleanup() {
|
package/bin/install-check.js
CHANGED
|
@@ -41,11 +41,67 @@ export function globalPrefixOf(pkgRoot) {
|
|
|
41
41
|
return basename(dirname(nm)) === 'lib' ? dirname(dirname(nm)) : null;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// registry:本次用哪个下载源(见下面 upgradeCandidates);装的是该源上的 latest
|
|
45
|
+
export function npmUpgradeCmd(pkgRoot, registry = REGISTRIES.official) {
|
|
45
46
|
const win = process.platform === 'win32';
|
|
46
47
|
const local = join(dirname(process.execPath), win ? 'npm.cmd' : 'npm');
|
|
47
48
|
const npm = existsSync(local) ? `"${local}"` : (win ? 'npm.cmd' : 'npm');
|
|
48
49
|
const prefix = globalPrefixOf(pkgRoot);
|
|
49
|
-
|
|
50
|
-
return `${npm} install -g @wendongfly/myhi@latest --registry https://registry.npmjs.org${prefix ? ` --prefix "${prefix}"` : ''}`;
|
|
50
|
+
return `${npm} install -g @wendongfly/myhi@latest --registry ${registry}${prefix ? ` --prefix "${prefix}"` : ''}`;
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
// ── 下载源:官方 / 国内镜像 自动兼容(myhi 升级、Claude 升级共用)──────────────
|
|
54
|
+
// 国内网络常连不上 / 很慢 registry.npmjs.org;而镜像同步有延迟(几分钟~几小时)。
|
|
55
|
+
// 「查新版」和「安装」必须对齐到同一个源,否则会「提示有新版、装上的却是旧版」。
|
|
56
|
+
// config.json 的 npmRegistry:auto(默认:先官方,连不上 / 装失败再国内镜像)/ official / mirror / 自定义 URL
|
|
57
|
+
export const REGISTRIES = { official: 'https://registry.npmjs.org', mirror: 'https://registry.npmmirror.com' };
|
|
58
|
+
export const registryName = (r) => r === REGISTRIES.official ? '官方源' : r === REGISTRIES.mirror ? '国内镜像' : r;
|
|
59
|
+
|
|
60
|
+
export function registryOrder(pref) {
|
|
61
|
+
if (pref === 'official') return [REGISTRIES.official];
|
|
62
|
+
if (pref === 'mirror') return [REGISTRIES.mirror];
|
|
63
|
+
if (pref && /^https?:\/\//.test(pref)) return [String(pref).replace(/\/+$/, '')];
|
|
64
|
+
return [REGISTRIES.official, REGISTRIES.mirror];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function fetchLatest(registry, pkg, ms = 8000) {
|
|
68
|
+
try {
|
|
69
|
+
const r = await fetch(`${registry}/${pkg}/latest`, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(ms) });
|
|
70
|
+
return r.ok ? (await r.json()).version || null : null;
|
|
71
|
+
} catch { return null; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function versionGt(a, b) {
|
|
75
|
+
const pa = String(a || '').split(/[.-]/).map(Number), pb = String(b || '').split(/[.-]/).map(Number);
|
|
76
|
+
for (let i = 0; i < 3; i++) { const x = pa[i] || 0, y = pb[i] || 0; if (x !== y) return x > y; }
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 查最新版:按顺序问各源,第一个答上来的为准 → { version, registry } | null */
|
|
81
|
+
export async function latestVersion(pkg, pref) {
|
|
82
|
+
for (const registry of registryOrder(pref)) {
|
|
83
|
+
const version = await fetchLatest(registry, pkg);
|
|
84
|
+
if (version) return { version, registry };
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 升级要尝试的源:比当前版本新的才算(镜像没同步到就跳过它)。
|
|
91
|
+
* → { candidates: [{registry, version}], reachable: [...], note }
|
|
92
|
+
* candidates 为空时 note 说明原因:都连不上 / 已是最新 / 镜像还没同步
|
|
93
|
+
*/
|
|
94
|
+
export async function upgradeCandidates(pkg, current, pref) {
|
|
95
|
+
const order = registryOrder(pref);
|
|
96
|
+
const got = await Promise.all(order.map(async (registry) => ({ registry, version: await fetchLatest(registry, pkg) })));
|
|
97
|
+
const reachable = got.filter((g) => g.version);
|
|
98
|
+
const candidates = reachable.filter((g) => !current || versionGt(g.version, current));
|
|
99
|
+
let note = null;
|
|
100
|
+
if (!candidates.length) {
|
|
101
|
+
if (!reachable.length) note = `${order.map(registryName).join('和')}${order.length > 1 ? '都' : ''}连不上,请检查网络(或在 设置 › 服务 › 升级下载源 换一个)`;
|
|
102
|
+
else note = `已经是最新版本 ${current}` + (reachable.some((g) => g.registry !== REGISTRIES.official) && !reachable.some((g) => g.registry === REGISTRIES.official)
|
|
103
|
+
? '(官方源连不上,以国内镜像为准;镜像可能还没同步到最新,过一会儿再试)' : '');
|
|
104
|
+
}
|
|
105
|
+
return { candidates, reachable, note };
|
|
106
|
+
}
|
|
107
|
+
|
|
Binary file
|
package/dist/chat.html
CHANGED
|
@@ -414,6 +414,9 @@
|
|
|
414
414
|
/* 文件名允许换行显全名(长报告名不再被掐断);动作收成图标省宽度 */
|
|
415
415
|
.fp-item .fp-name { flex: 1; min-width: 0; word-break: break-all; line-height: 1.35; }
|
|
416
416
|
.fp-item .fp-size { color: var(--muted); font-size: 0.7rem; white-space: nowrap; align-self: flex-start; padding-top: 0.05rem; }
|
|
417
|
+
.fp-item .fp-chk { width: 1rem; height: 1rem; flex-shrink: 0; align-self: flex-start; margin-top: 0.1rem; accent-color: var(--purple); }
|
|
418
|
+
.fp-item.sel { background: var(--surface2); }
|
|
419
|
+
#fp-multi-btn.on { border-color: var(--purple); color: var(--purple); }
|
|
417
420
|
.fp-item .fp-dl { color: var(--blue); font-size: 0.78rem; text-decoration: none; padding: 0.2rem 0.3rem; flex-shrink: 0; align-self: flex-start; }
|
|
418
421
|
.fp-empty { text-align: center; color: var(--muted); padding: 2rem 0.75rem; font-size: 0.85rem; }
|
|
419
422
|
.fp-drop-zone {
|
|
@@ -837,8 +840,16 @@
|
|
|
837
840
|
<div id="fp-tools" style="display:flex;gap:0.4rem;align-items:center;padding:0.3rem 0.75rem;flex-wrap:wrap">
|
|
838
841
|
<button class="fp-btn" onclick="fpOpenFolder()" title="在本机桌面打开该任务的文件夹——仅当浏览器与 myhi 服务在同一台机器时有效;远程访问不会有效果,请用下方列表浏览/下载">📂 打开当前文件夹(不支持远程)</button>
|
|
839
842
|
<span style="flex:1"></span>
|
|
843
|
+
<button class="fp-btn" id="fp-multi-btn" onclick="fpToggleMulti()" title="勾选多个文件 / 文件夹,打包成一个 zip 下载">☑ 多选</button>
|
|
840
844
|
<button class="fp-btn" id="fp-sort-btn" onclick="fpToggleSort()" title="切换排序">排序: 名称</button>
|
|
841
845
|
</div>
|
|
846
|
+
<div id="fp-sel-bar" style="display:none;align-items:center;gap:0.4rem;padding:0.35rem 0.75rem;background:var(--surface2);font-size:0.78rem;flex-wrap:wrap">
|
|
847
|
+
<span id="fp-sel-count" style="color:var(--muted)">已选 0 项</span>
|
|
848
|
+
<span style="flex:1"></span>
|
|
849
|
+
<button class="fp-btn" onclick="fpSelAll()">全选</button>
|
|
850
|
+
<button class="fp-btn fp-btn-primary" id="fp-sel-dl" onclick="fpZipSelected()" disabled>⬇ 打包下载</button>
|
|
851
|
+
<button class="fp-btn" onclick="fpToggleMulti(false)">取消</button>
|
|
852
|
+
</div>
|
|
842
853
|
<div class="fp-list" id="fp-list"></div>
|
|
843
854
|
<div class="fp-diff-actions" id="fp-diff-actions" style="display:none">
|
|
844
855
|
<button class="fp-btn fp-btn-primary" onclick="fpSyncToLocal()">下载到本地</button>
|
|
@@ -2059,8 +2070,19 @@
|
|
|
2059
2070
|
if (t.startsWith('<command-name>')) return 'command';
|
|
2060
2071
|
if (t.startsWith('<local-command-caveat>') || t.startsWith('Caveat:')) return 'caveat';
|
|
2061
2072
|
if (t.startsWith('<system-reminder>') || t.startsWith('<local-command-stdout>')) return 'noise';
|
|
2073
|
+
// 后台任务(AI 放到后台跑的命令 / 子代理)结束时 claude 自动插给 AI 的通知,不是用户发言
|
|
2074
|
+
if (t.startsWith('<task-notification>')) return 'task';
|
|
2062
2075
|
return null;
|
|
2063
2076
|
}
|
|
2077
|
+
// <task-notification> → 一行人话:「⚙ 后台任务「…」已完成 / 已停止 / 失败」
|
|
2078
|
+
function taskNotificationText(text) {
|
|
2079
|
+
const tag = (n) => ((text.match(new RegExp('<' + n + '>([\\s\\S]*?)</' + n + '>')) || [])[1] || '').trim();
|
|
2080
|
+
const st = tag('status');
|
|
2081
|
+
const word = { completed: '已完成', killed: '已停止(到达时间上限或被中止)', failed: '失败', stopped: '已停止' }[st] || (st || '有新状态');
|
|
2082
|
+
const sum = tag('summary');
|
|
2083
|
+
const what = (sum.match(/"([^"]+)"/) || [])[1] || sum; // 摘要里引号括起的是任务名
|
|
2084
|
+
return `⚙ ${/^Agent\b/i.test(sum) ? '子代理' : '后台任务'}${what ? '「' + what.slice(0, 80) + '」' : ''}${word}`;
|
|
2085
|
+
}
|
|
2064
2086
|
|
|
2065
2087
|
function buildSyntheticUserEl(content) {
|
|
2066
2088
|
const el = document.createElement('div');
|
|
@@ -2070,6 +2092,12 @@
|
|
|
2070
2092
|
el.textContent = '— ⏹ 请求已中断 —';
|
|
2071
2093
|
return el;
|
|
2072
2094
|
}
|
|
2095
|
+
if (kind === 'task') {
|
|
2096
|
+
el.className = 'msg msg-status';
|
|
2097
|
+
el.textContent = '— ' + taskNotificationText(content) + ' —';
|
|
2098
|
+
el.title = '这是 AI 放到后台运行的任务结束时,系统自动发给 AI 的通知,不是你发的消息';
|
|
2099
|
+
return el;
|
|
2100
|
+
}
|
|
2073
2101
|
if (kind === 'command') {
|
|
2074
2102
|
// 本地命令回显:状态行显示执行了什么命令(用户自己发的命令已有气泡,这是 claude 的回显)
|
|
2075
2103
|
el.className = 'msg msg-status';
|
|
@@ -3127,9 +3155,9 @@
|
|
|
3127
3155
|
if (kind === 'compact' || kind === 'interrupt') {
|
|
3128
3156
|
const sel = buildSyntheticUserEl(msg.content);
|
|
3129
3157
|
if (sel) { chatArea.appendChild(sel); trimMessages(); scrollToBottom(); }
|
|
3130
|
-
} else if (kind === 'command') {
|
|
3158
|
+
} else if (kind === 'command' || kind === 'task') {
|
|
3131
3159
|
const sel = buildSyntheticUserEl(msg.content);
|
|
3132
|
-
if (sel) addStatusMessage(sel.textContent);
|
|
3160
|
+
if (sel) addStatusMessage(sel.textContent.replace(/^— | —$/g, ''));
|
|
3133
3161
|
} else if (!kind) {
|
|
3134
3162
|
addInputMessage(msg.content, msg.inject);
|
|
3135
3163
|
}
|
|
@@ -4118,10 +4146,18 @@
|
|
|
4118
4146
|
const entries = (data.entries || []).slice();
|
|
4119
4147
|
if (fpSort === 'recent') entries.sort((a, b) => (b.mtime || 0) - (a.mtime || 0)); // 最近修改在前,看 AI 刚生成啥
|
|
4120
4148
|
else entries.sort((a, b) => (Number(b.isDir) - Number(a.isDir)) || a.name.localeCompare(b.name));
|
|
4149
|
+
fpVisible = [];
|
|
4121
4150
|
for (const e of entries) {
|
|
4122
4151
|
const rel = fpCurrentPath ? fpCurrentPath + '/' + e.name : e.name;
|
|
4152
|
+
fpVisible.push(rel);
|
|
4153
|
+
if (fpSel) {
|
|
4154
|
+
// 多选模式:点整行 = 勾选 / 取消,不进入文件夹、不打开编辑
|
|
4155
|
+
const on = fpSel.has(rel);
|
|
4156
|
+
html += `<div class="fp-item${on ? ' sel' : ''}" onclick="fpToggleSel('${escAttr(rel)}')"><input type="checkbox" class="fp-chk" ${on ? 'checked' : ''} tabindex="-1" style="pointer-events:none"><span class="fp-icon">${e.isDir ? '📁' : '📄'}</span><span class="fp-name" title="${escAttr(rel)}">${escHtml(e.name)}</span>${e.isDir ? '' : `<span class="fp-size">${fpFormatSize(e.size)}</span>`}</div>`;
|
|
4157
|
+
continue;
|
|
4158
|
+
}
|
|
4123
4159
|
if (e.isDir) {
|
|
4124
|
-
html += `<div class="fp-item" onclick="fpNav('${escHtml(rel)}')"><span class="fp-icon">📁</span><span class="fp-name" title="${escAttr(rel)}">${escHtml(e.name)}</span><span class="fp-dl" onclick="event.stopPropagation();fpRefer('${escAttr(rel)}/')" title="引用:把目录路径插入消息框">@</span></div>`;
|
|
4160
|
+
html += `<div class="fp-item" onclick="fpNav('${escHtml(rel)}')"><span class="fp-icon">📁</span><span class="fp-name" title="${escAttr(rel)}">${escHtml(e.name)}</span><span class="fp-dl" onclick="event.stopPropagation();fpRefer('${escAttr(rel)}/')" title="引用:把目录路径插入消息框">@</span><span class="fp-dl" onclick="event.stopPropagation();fpZip(['${escAttr(rel)}'])" title="打包下载这个文件夹(zip)">⬇</span></div>`;
|
|
4125
4161
|
} else {
|
|
4126
4162
|
const editable = isEditableFile(e.name);
|
|
4127
4163
|
html += `<div class="fp-item" ${editable ? `style="cursor:pointer" onclick="openFileEditor('${escAttr(rel)}')" title="点击在线编辑"` : ''}>
|
|
@@ -4140,7 +4176,52 @@
|
|
|
4140
4176
|
}
|
|
4141
4177
|
}
|
|
4142
4178
|
|
|
4143
|
-
window.fpNav = function(path) { fpCurrentPath = path; fpLoadFiles(); };
|
|
4179
|
+
window.fpNav = function(path) { fpCurrentPath = path; if (fpSel) fpSel.clear(); fpSyncSelBar(); fpLoadFiles(); };
|
|
4180
|
+
|
|
4181
|
+
// ── 文件夹 / 多选打包下载(zip):先登记(服务端算好文件数、大小,超限当场提示),再触发浏览器下载 ──
|
|
4182
|
+
let fpSel = null; // 多选模式时是 Set(相对路径),否则 null
|
|
4183
|
+
let fpVisible = []; // 当前目录列出的条目(全选用)
|
|
4184
|
+
function fpSyncSelBar() {
|
|
4185
|
+
const bar = document.getElementById('fp-sel-bar');
|
|
4186
|
+
bar.style.display = fpSel ? 'flex' : 'none';
|
|
4187
|
+
document.getElementById('fp-multi-btn').classList.toggle('on', !!fpSel);
|
|
4188
|
+
if (!fpSel) return;
|
|
4189
|
+
document.getElementById('fp-sel-count').textContent = `已选 ${fpSel.size} 项`;
|
|
4190
|
+
document.getElementById('fp-sel-dl').disabled = !fpSel.size;
|
|
4191
|
+
}
|
|
4192
|
+
window.fpToggleMulti = function(on) {
|
|
4193
|
+
fpSel = (on ?? !fpSel) ? new Set() : null;
|
|
4194
|
+
fpSyncSelBar(); fpLoadFiles();
|
|
4195
|
+
};
|
|
4196
|
+
window.fpToggleSel = function(rel) {
|
|
4197
|
+
if (!fpSel) return;
|
|
4198
|
+
fpSel.has(rel) ? fpSel.delete(rel) : fpSel.add(rel);
|
|
4199
|
+
fpSyncSelBar(); fpLoadFiles();
|
|
4200
|
+
};
|
|
4201
|
+
window.fpSelAll = function() {
|
|
4202
|
+
if (!fpSel) return;
|
|
4203
|
+
const all = fpVisible.every(r => fpSel.has(r));
|
|
4204
|
+
fpVisible.forEach(r => all ? fpSel.delete(r) : fpSel.add(r));
|
|
4205
|
+
fpSyncSelBar(); fpLoadFiles();
|
|
4206
|
+
};
|
|
4207
|
+
window.fpZipSelected = function() { if (fpSel?.size) fpZip([...fpSel]); };
|
|
4208
|
+
window.fpZip = async function(paths) {
|
|
4209
|
+
const prog = document.getElementById('fp-progress');
|
|
4210
|
+
prog.textContent = '正在统计要打包的文件…'; prog.style.display = 'block';
|
|
4211
|
+
let d;
|
|
4212
|
+
try {
|
|
4213
|
+
const r = await fetch(`/api/files/${SESSION_ID}/zip-ticket`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths }) });
|
|
4214
|
+
d = await r.json();
|
|
4215
|
+
} catch (e) { d = { ok: false, error: e.message }; }
|
|
4216
|
+
if (!d.ok) { prog.textContent = '打包下载失败:' + (d.error || ''); setTimeout(() => { prog.style.display = 'none'; }, 6000); return; }
|
|
4217
|
+
const a = document.createElement('a');
|
|
4218
|
+
a.href = `/api/files/${SESSION_ID}/zip?ticket=${encodeURIComponent(d.ticket)}`; a.download = d.name;
|
|
4219
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
4220
|
+
prog.textContent = `开始下载「${d.name}」:${d.files} 个文件,共 ${fpFormatSize(d.bytes)}(打包前)` +
|
|
4221
|
+
(d.skipped?.length ? `;已跳过 ${d.skipped.join('、')}(可重新生成的目录)` : '') + '。文件多时浏览器要等一会儿才开始显示进度。';
|
|
4222
|
+
setTimeout(() => { prog.style.display = 'none'; }, 9000);
|
|
4223
|
+
if (fpSel) fpToggleMulti(false);
|
|
4224
|
+
};
|
|
4144
4225
|
|
|
4145
4226
|
// 把文本插进消息框光标处(前面没空白就补一个,后面留一个空格方便接着打字)
|
|
4146
4227
|
function insertIntoInput(text) {
|
|
@@ -4722,6 +4803,18 @@
|
|
|
4722
4803
|
window.goHome = goHome;
|
|
4723
4804
|
window.goBack = function() { goHome(); };
|
|
4724
4805
|
|
|
4806
|
+
// ── 电脑宽屏回工作台 ──────────────────────────────────────────
|
|
4807
|
+
// 工作台窗口拉窄(< 1024px,比如两个窗口并排、拖到半屏)会跳到单独会话页;以前再拉宽也回不去,
|
|
4808
|
+
// 一直停在没有左侧任务栏的页面。现在:电脑(鼠标)上单独打开的会话页,宽度够了就回工作台、仍选中这个任务。
|
|
4809
|
+
// 分屏(embed=1)、工作台右侧(embed=ws)里嵌的会话页不动
|
|
4810
|
+
{
|
|
4811
|
+
const wide = matchMedia('(min-width: 1024px) and (pointer: fine)');
|
|
4812
|
+
const standalone = !new URLSearchParams(location.search).get('embed') && window.top === window;
|
|
4813
|
+
const back = () => { if (standalone && wide.matches) location.replace('/?s=' + encodeURIComponent(SESSION_ID)); };
|
|
4814
|
+
wide.addEventListener('change', back);
|
|
4815
|
+
back();
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4725
4818
|
// ── 手机精简布局:⋯ 更多菜单 / 技能按钮 / 未读标记 / 其他任务提醒 ──────────
|
|
4726
4819
|
const IS_M = document.documentElement.classList.contains('m');
|
|
4727
4820
|
// 技能按钮:手机上快捷栏不平铺技能,点「技能」直接出可运行列表(底部有「管理技能」)
|
|
@@ -4779,8 +4872,15 @@
|
|
|
4779
4872
|
|
|
4780
4873
|
// 本页在看 = 本任务已读(切换面板 / 首页据此算「新完成」)
|
|
4781
4874
|
let _seenAt = 0;
|
|
4875
|
+
// 真的在屏幕上才算「看过」:工作台把最近 5 个任务页藏在后台(display:none 的 iframe),
|
|
4876
|
+
// 它们的 document.hidden 仍是 false,不判断的话这几个任务永远不会显示「新完成」
|
|
4877
|
+
function pageOnScreen() {
|
|
4878
|
+
if (document.hidden) return false;
|
|
4879
|
+
try { if (window.frameElement && !window.frameElement.getClientRects().length) return false; } catch {}
|
|
4880
|
+
return true;
|
|
4881
|
+
}
|
|
4782
4882
|
function markSeenNow(force) {
|
|
4783
|
-
if (
|
|
4883
|
+
if (!pageOnScreen() || !window.myhiAttn) return;
|
|
4784
4884
|
if (!force && Date.now() - _seenAt < 2000) return;
|
|
4785
4885
|
_seenAt = Date.now(); myhiAttn.markSeen(SESSION_ID);
|
|
4786
4886
|
}
|
|
Binary file
|
|
Binary file
|
package/dist/index.html
CHANGED
|
@@ -968,6 +968,11 @@
|
|
|
968
968
|
<div class="set-seg" id="set-proto"><button data-p="https">HTTPS</button><button data-p="http">HTTP</button></div>
|
|
969
969
|
</div>
|
|
970
970
|
<div id="set-proto-status" class="set-status"></div>
|
|
971
|
+
<div class="set-item admin-only">
|
|
972
|
+
<div class="set-lbl">升级下载源<small>myhi 和 Claude 升级从哪下载。自动:先官方源,连不上或装失败自动换国内镜像</small></div>
|
|
973
|
+
<div class="set-seg" id="set-reg"><button data-r="auto">自动</button><button data-r="official">官方</button><button data-r="mirror">国内镜像</button></div>
|
|
974
|
+
</div>
|
|
975
|
+
<div id="set-reg-status" class="set-status"></div>
|
|
971
976
|
<a id="set-admin-link" class="set-link admin-only" href="/admin">🛠 管理后台 <small>用户 · 网关 · 密钥 · Claude 升级</small></a>
|
|
972
977
|
<button class="set-link" onclick="restartMyhi()">🔄 重启 myhi</button>
|
|
973
978
|
</div>
|
|
@@ -1005,7 +1010,7 @@
|
|
|
1005
1010
|
<div id="sheet-backdrop" onclick="closeSheet()"></div>
|
|
1006
1011
|
<div id="sheet">
|
|
1007
1012
|
<div class="sheet-handle"></div>
|
|
1008
|
-
<div class="sheet-title"
|
|
1013
|
+
<div class="sheet-title">新建任务</div>
|
|
1009
1014
|
|
|
1010
1015
|
<!-- Preset chips -->
|
|
1011
1016
|
<div class="presets" id="presets"></div>
|
|
@@ -1014,7 +1019,8 @@
|
|
|
1014
1019
|
<label>名称 <span style="font-weight:400;text-transform:none;color:var(--red,var(--red))">必填</span></label>
|
|
1015
1020
|
<input id="inp-title" type="text" placeholder="请输入任务名称" autocomplete="off" spellcheck="false">
|
|
1016
1021
|
</div>
|
|
1017
|
-
|
|
1022
|
+
<!-- 启动命令:默认就是 AI 任务(claude),Shell 直接开终端,都不需要填 —— 隐藏,按默认来 -->
|
|
1023
|
+
<div class="field" style="display:none">
|
|
1018
1024
|
<label>启动命令 <span style="font-weight:400;text-transform:none">(可选)</span></label>
|
|
1019
1025
|
<input id="inp-cmd" type="text" placeholder="留空仅打开 shell" autocomplete="off" spellcheck="false" style="font-family:'SF Mono',Consolas,monospace;font-size:0.85rem">
|
|
1020
1026
|
</div>
|
|
@@ -1309,7 +1315,7 @@
|
|
|
1309
1315
|
window.openSettings = function() {
|
|
1310
1316
|
renderAppearance();
|
|
1311
1317
|
['set-machine-status', 'set-asr-status', 'set-restart-status', 'set-up-status', 'set-proto-status'].filter(id => document.getElementById(id)).forEach(id => setStatus(id, ''));
|
|
1312
|
-
if (_isAdmin) loadProto();
|
|
1318
|
+
if (_isAdmin) { loadProto(); loadRegistry(); }
|
|
1313
1319
|
document.querySelectorAll('#set-sheet .set-btn').forEach(b => b.classList.remove('dirty'));
|
|
1314
1320
|
document.getElementById('set-backdrop').classList.add('open');
|
|
1315
1321
|
document.getElementById('set-sheet').classList.add('open');
|
|
@@ -1474,6 +1480,20 @@
|
|
|
1474
1480
|
});
|
|
1475
1481
|
if (d.fromEnv) setStatus('set-proto-status', '启动时设了环境变量 MYHI_NO_HTTPS,固定为 HTTP');
|
|
1476
1482
|
}
|
|
1483
|
+
async function loadRegistry() {
|
|
1484
|
+
let d; try { d = await (await fetch('/api/npm-registry')).json(); } catch { return; }
|
|
1485
|
+
const seg = document.getElementById('set-reg'); if (!seg || !d.ok) return;
|
|
1486
|
+
const custom = !['auto', 'official', 'mirror'].includes(d.pref);
|
|
1487
|
+
const mark = (p) => seg.querySelectorAll('button').forEach(b => b.classList.toggle('on', b.dataset.r === p));
|
|
1488
|
+
mark(d.pref);
|
|
1489
|
+
if (custom) setStatus('set-reg-status', '当前用的是 config.json 里自定义的源:' + d.pref);
|
|
1490
|
+
seg.querySelectorAll('button').forEach(b => b.onclick = async () => {
|
|
1491
|
+
let r; try { r = await (await fetch('/api/npm-registry', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pref: b.dataset.r }) })).json(); } catch (e) { r = { ok: false, error: e.message }; }
|
|
1492
|
+
if (!r.ok) { setStatus('set-reg-status', '保存失败:' + (r.error || '')); return; }
|
|
1493
|
+
mark(r.pref);
|
|
1494
|
+
setStatus('set-reg-status', { auto: '已设为自动:先官方源,不行再国内镜像', official: '已设为只用官方源(registry.npmjs.org)', mirror: '已设为只用国内镜像(registry.npmmirror.com,可能比官方晚几分钟到几小时)' }[r.pref]);
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1477
1497
|
async function switchProto(httpOnly, cur) {
|
|
1478
1498
|
if (httpOnly === cur.httpOnly) return;
|
|
1479
1499
|
const url = `${httpOnly ? 'http' : 'https'}://${location.hostname}:${cur.port}/`;
|
|
@@ -1622,7 +1642,8 @@
|
|
|
1622
1642
|
const sub = esc([fmtCwd(s.cwd), fmtRelTime(sessionTime(s)), s.viewers > 0 ? `${s.viewers} 在线` : '', s.controlHolderName ? `${s.controlHolderName} 控制中` : ''].filter(Boolean).join(' · '));
|
|
1623
1643
|
const tags = (s.tags || []).map(t => `<span class="tag-chip">${esc(t)}</span>`).join('');
|
|
1624
1644
|
// 会话名片:一堆会话都叫 claude,靠第一条真人消息认出「这个任务当初是干啥的」
|
|
1625
|
-
|
|
1645
|
+
let at = window.myhiAttn ? myhiAttn.of(s) : '';
|
|
1646
|
+
if (at === 'done' && s.id === _wsCur && wsOn()) at = ''; // 工作台右边正开着的任务:你就在看它,不算「新完成」
|
|
1626
1647
|
// 等你回复 / 新完成:比起「当初问了啥」,AI 最后说了啥更有用
|
|
1627
1648
|
const gist = (at === 'wait' || at === 'done') && s.lastText
|
|
1628
1649
|
? `<div class="card-gist" style="font-size:0.72rem;color:var(--muted,var(--muted));margin-top:0.18rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:0.85">🤖 ${esc(s.lastText)}</div>`
|
|
@@ -1961,14 +1982,16 @@
|
|
|
1961
1982
|
what.innerHTML = `<b>包里会有</b>
|
|
1962
1983
|
<div class="mig-row"><span>📁</span><div>项目文件 <b>${p.files}</b> 个,共 <b>${fmtSize(p.bytes)}</b><br><small>${esc(p.cwd)}</small>${p.truncated ? '<div class="mig-warn">文件太多,只打包了前 20 万个</div>' : ''}</div></div>
|
|
1963
1984
|
${skipped ? `<div class="mig-row"><span>⏭</span><div>不打包(可重新生成,到新电脑再装):${skipped}</div></div>` : ''}
|
|
1964
|
-
<div class="mig-row"><span>💬</span><label style="cursor:pointer"><input type="checkbox" id="mig-conv" ${p.hasConv ? 'checked' : 'disabled'}> 带上 AI 对话记录${p.hasConv ? `(${fmtSize(p.convBytes)})` : ''}<br><small>${p.hasConv ? '带上后在新电脑上可以<b>接着聊</b>,AI
|
|
1985
|
+
<div class="mig-row"><span>💬</span><label style="cursor:pointer"><input type="checkbox" id="mig-conv" ${p.hasConv ? 'checked' : 'disabled'}> 带上 AI 对话记录${p.hasConv ? `(${fmtSize(p.convBytes)})` : ''}<br><small>${p.hasConv ? '带上后在新电脑上可以<b>接着聊</b>,AI 记得之前做过什么;对话里的项目路径会自动改成新位置。' : '这个任务还没有对话记录。'}</small></label></div>
|
|
1986
|
+
<div class="mig-row"><span>🧠</span><label style="cursor:pointer"><input type="checkbox" id="mig-mem" ${p.memFiles ? 'checked' : 'disabled'}> 带上项目记忆${p.memFiles ? `(${p.memFiles} 个文件)` : ''}<br><small>${p.memFiles ? 'AI 在这个项目里记下的约定、经验(记忆功能),和对话记录分开选:只要记忆、不要冗长聊天记录也行。记忆里写到的旧路径会改成新位置。' : '这个项目还没有 AI 记忆。'}</small></label></div>
|
|
1965
1987
|
${p.guard ? `<div class="mig-row"><span>🛡</span><div>隐私护栏配置(${p.guard.enabled ? '已开启' : '未开启'},主体词典 ${p.guard.entities} 个)和假名映射表一起带走,新电脑上继续脱敏</div></div>` : ''}
|
|
1966
1988
|
${p.secrets.length ? `<div class="mig-row"><span>🔑</span><div><b>密钥不会带走</b>(${p.secrets.map(esc).join('、')})<br><small>为了安全,密钥只留在这台电脑。到新电脑后在任务会话页「🔑 密钥」里重新录入。</small></div></div>` : ''}
|
|
1967
1989
|
${p.busy ? '<div class="mig-row mig-warn"><span>⚠️</span><div>AI 正在执行,建议等这一轮结束再导出,否则最后一轮对话可能不完整。</div></div>' : ''}`;
|
|
1968
1990
|
const btn = box.querySelector('#mig-dl'); btn.disabled = false;
|
|
1969
1991
|
btn.onclick = () => {
|
|
1970
1992
|
const conv = box.querySelector('#mig-conv')?.checked ? '1' : '0';
|
|
1971
|
-
const
|
|
1993
|
+
const mem = box.querySelector('#mig-mem')?.checked ? '1' : '0';
|
|
1994
|
+
const a = document.createElement('a'); a.href = '/api/migrate/' + encodeURIComponent(id) + '/export?conv=' + conv + '&mem=' + mem; a.download = '';
|
|
1972
1995
|
document.body.appendChild(a); a.click(); a.remove();
|
|
1973
1996
|
btn.textContent = '✓ 已开始下载'; btn.disabled = true;
|
|
1974
1997
|
btn.insertAdjacentHTML('afterend', '<div class="mig-dim" style="margin-top:0.4rem">文件大时浏览器会在后台下载,留意下载栏 / 通知。下载完成后按上面第 2、3 步操作。</div>');
|
|
@@ -2011,6 +2034,7 @@
|
|
|
2011
2034
|
<div class="mig-row"><span>📝</span><div>任务:<b>${esc(m.title || '')}</b><br><small>来自「${esc(m.from || '另一台 myhi')}」,${esc(new Date(m.exportedAt).toLocaleString())} 导出</small></div></div>
|
|
2012
2035
|
<div class="mig-row"><span>📁</span><div>项目文件 ${m.files} 个,共 ${fmtSize(m.bytes || 0)}<br><small>原位置:${esc(m.cwd || '')}</small></div></div>
|
|
2013
2036
|
<div class="mig-row"><span>💬</span><div>${m.conv ? 'AI 对话记录:有 —— 导入后可以<b>接着聊</b>' : 'AI 对话记录:没带 —— 导入后是一个新对话(项目文件都在)'}</div></div>
|
|
2037
|
+
<div class="mig-row"><span>🧠</span><div>${m.memory ? `项目记忆:${m.memory.count} 个文件 —— AI 记下的项目约定和经验一起导入;这台电脑上已有同名记忆时保留这台的,记忆索引会合并` : '项目记忆:没带'}</div></div>
|
|
2014
2038
|
${skipped ? `<div class="mig-row"><span>⏭</span><div>原电脑上没打包:${skipped}<br><small>依赖 / 构建产物需要在这台电脑重新安装或生成(例如 npm install);可以导入后直接让 AI 帮你装。</small></div></div>` : ''}
|
|
2015
2039
|
${m.guard ? `<div class="mig-row"><span>🛡</span><div>隐私护栏配置一起导入(主体词典 ${m.guard.entities} 个)</div></div>` : ''}
|
|
2016
2040
|
${(m.secrets || []).length ? `<div class="mig-row mig-warn"><span>🔑</span><div>需要重新录入的密钥:${m.secrets.map(esc).join('、')}<br><small>导入后在任务会话页「🔑 密钥」里填。</small></div></div>` : ''}
|
|
@@ -2029,7 +2053,8 @@
|
|
|
2029
2053
|
catch (e) { d = { ok: false, error: e.message }; }
|
|
2030
2054
|
if (!d.ok) { btn.disabled = false; btn.textContent = '导入为新任务'; res.innerHTML = '<div class="mig-warn" style="margin-top:0.4rem">' + esc(d.error) + '</div>'; return; }
|
|
2031
2055
|
btn.textContent = '✓ 导入完成';
|
|
2032
|
-
|
|
2056
|
+
const mm = d.memory && (d.memory.written || d.memory.kept) ? `项目记忆导入 ${d.memory.written} 个${d.memory.kept ? `(${d.memory.kept} 个这台电脑上已有,保留了这台的)` : ''}。` : '';
|
|
2057
|
+
res.innerHTML = `<div style="margin-top:0.5rem">任务已导入到 <code>${esc(cwd)}</code>。${d.conv ? '打开后直接发消息就能接着聊。' : ''}${mm}${(d.secrets || []).length ? `<div class="mig-warn">别忘了在会话页「🔑 密钥」里重新录入:${d.secrets.map(esc).join('、')}</div>` : ''}</div>
|
|
2033
2058
|
<button class="mig-btn" id="mig-open">打开这个任务</button>`;
|
|
2034
2059
|
res.querySelector('#mig-open').onclick = () => { document.getElementById('mig-ov')?.remove(); openSession(d.id); };
|
|
2035
2060
|
try { socket.emit('list'); } catch {}
|
|
@@ -2242,8 +2267,7 @@
|
|
|
2242
2267
|
document.querySelectorAll('.chip').forEach((c, j) =>
|
|
2243
2268
|
c.classList.toggle('selected', j === i));
|
|
2244
2269
|
const p = PRESETS[i];
|
|
2245
|
-
// 不再自动把名称填成 claude/shell ——
|
|
2246
|
-
document.getElementById('inp-cmd').value = p.cmd || '';
|
|
2270
|
+
// 不再自动把名称填成 claude/shell —— 名称必填、留给用户输入任务名;启动命令字段已隐藏,不再回填
|
|
2247
2271
|
}
|
|
2248
2272
|
|
|
2249
2273
|
// ── Create sheet ─────────────────────────────────────────────
|
|
@@ -2323,7 +2347,7 @@
|
|
|
2323
2347
|
titleInput.focus();
|
|
2324
2348
|
return;
|
|
2325
2349
|
}
|
|
2326
|
-
const initCmd =
|
|
2350
|
+
const initCmd = undefined; // 启动命令已隐藏:AI 任务不用,Shell 直接开终端
|
|
2327
2351
|
const cwd = document.getElementById('inp-cwd').value.trim() || undefined;
|
|
2328
2352
|
if (cwd) recordDir(cwd);
|
|
2329
2353
|
const preset = selectedPreset != null ? PRESETS[selectedPreset] : null;
|
|
Binary file
|