@wendongfly/myhi 1.3.116 → 1.3.121
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/README.md +34 -1
- package/dist/chat.html +201 -1
- package/dist/grid.html +5 -3
- package/dist/index.html +2 -0
- package/dist/index.js +1 -1
- package/dist/index.min.js +142 -140
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
- **双前端界面** —— 聊天对话式 UI(适合手机触屏)和 xterm.js 全功能终端
|
|
12
12
|
- **设备控制权切换** —— 多设备同时观看,单设备独占输入,支持接管与释放
|
|
13
13
|
- **AI Agent 集成** —— 内置 Claude Code CLI,Web 界面中直接与 AI 对话编程、审批文件变更
|
|
14
|
+
- **🛡 隐私护栏(去标识化)** —— 提交给模型的敏感实体(主体名、证件/卡号/手机等)自动假名化,模型/第三方网关只看到占位符,答案里再还原成真名,映射表只留本机 + 审计留痕可导出
|
|
14
15
|
- **角色权限管理** —— admin / operator / viewer 三级角色,多种认证模式
|
|
15
16
|
- **多用户隔离** —— 独立用户目录、独立 Git 配置,支持独占模式
|
|
16
17
|
- **SSH 开发账号** —— 为每个用户创建独立的 SSH 账号,VS Code Remote-SSH 远程开发
|
|
@@ -344,6 +345,36 @@ myhi 支持在 Web 界面中检测和执行版本更新。
|
|
|
344
345
|
|
|
345
346
|
---
|
|
346
347
|
|
|
348
|
+
## 🛡 隐私护栏(去标识化 / 可逆假名化)
|
|
349
|
+
|
|
350
|
+
客户提交的提示词/数据常含**主体实体、证件号、卡号、手机**等敏感信息。直接发给模型(尤其经第三方 AI 网关)就是一条泄露面。myhi 在**客户端 ↔ 模型之间的唯一咽喉**做去标识化,形成三层敏感信息防护:
|
|
351
|
+
|
|
352
|
+
| 层 | 保护对象 | 机制 |
|
|
353
|
+
|----|----------|------|
|
|
354
|
+
| Layer A | 密钥(PAT/token/证书) | 注入成环境变量,命令用 `$NAME` 引用,模型不见真值 |
|
|
355
|
+
| Layer B | 密钥 | 落 history/广播时精确脱敏成 `«SECRET:NAME»` 兜底 |
|
|
356
|
+
| **Layer C · 隐私护栏** | **客户实体/数据** | **出模型前换占位符、回模型后还原真名** |
|
|
357
|
+
|
|
358
|
+
**工作方式**(Layer C,可逆假名化):
|
|
359
|
+
|
|
360
|
+
1. **出模型前**:检测敏感实体 → 替换成稳定占位符 `【类型_N】`,模型 / 第三方网关**只看到假名**;
|
|
361
|
+
2. **回模型后**:把答案里的占位符**自动还原**成真名,用户体验无损;
|
|
362
|
+
3. **映射表只留本机**:`~/.myhi/pseudo-maps/<会话>.json`(`chmod 600`),跨轮累积(同一实体全程同一占位符)、跨 `--resume` 持久,**绝不进会话/网关/记忆**。
|
|
363
|
+
|
|
364
|
+
**检测能力**:
|
|
365
|
+
|
|
366
|
+
- **高危 PII 正则**:身份证、统一社会信用代码、银行卡、手机、邮箱、车牌;
|
|
367
|
+
- **项目主体词典**:自录「我方主体 / 关联公司 / 关键人」名单,命中即脱敏(最实用一档);
|
|
368
|
+
- **金额**(严格档)。
|
|
369
|
+
|
|
370
|
+
三档 `仅高危 / 标准 / 严格`,默认关闭(opt-in)。在会话页快捷栏「🛡 护栏」抽屉自助维护(开关 / 档位 / 词典增删批量)。
|
|
371
|
+
|
|
372
|
+
**审计留痕**:每个有脱敏的轮次记录「时间 / 脱敏数量 / 类型分布 / 发给模型的脱敏后全文」——**只含假名文本、不含真值**,可在护栏抽屉「📋 审计」查看并**一键导出**给合规方取证,证明「模型这一轮实际看到的就是这段假名」。
|
|
373
|
+
|
|
374
|
+
> **⚠️ 边界**:护栏只保护**用户经对话框提交的内容**。若让 AI 用 Read/Bash 工具去读磁盘上的原始数据文件,`tool_result` 是在 Claude 进程内部直送模型的,myhi 拦不到——要保护就把数据**贴进对话框**,别让 AI 去读原始文件。
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
347
378
|
## 网络与连接
|
|
348
379
|
|
|
349
380
|
### 局域网
|
|
@@ -550,7 +581,9 @@ myhi/
|
|
|
550
581
|
├── src/
|
|
551
582
|
│ ├── server.js # 主服务器:路由、Socket.IO、认证、Git API
|
|
552
583
|
│ ├── sessions.js # 会话管理:PTY、Agent、持久化
|
|
553
|
-
│ ├── agent.js # Claude Code CLI
|
|
584
|
+
│ ├── agent.js # Claude Code CLI 集成(出向脱敏 / 入向还原落点)
|
|
585
|
+
│ ├── secrets.js # 密钥库 Layer A/B(env 注入 + 精确脱敏)
|
|
586
|
+
│ ├── pseudo.js # 🛡 隐私护栏 Layer C(去标识化 / 可逆假名化 / 审计留痕)
|
|
554
587
|
│ ├── roles.js # 角色与用户管理
|
|
555
588
|
│ └── attach.js # CLI 附加客户端
|
|
556
589
|
├── public/
|
package/dist/chat.html
CHANGED
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
html[data-font="large"] { font-size: 19.5px; }
|
|
53
53
|
/* 分屏内嵌模式(?embed=1):隐藏本页顶栏,让每个窗格更紧凑(外层 grid.html 提供窗格头) */
|
|
54
54
|
html.embed #top-bar { display: none; }
|
|
55
|
+
/* 分屏是 PC 大屏功能,手机端(触屏)隐藏入口 */
|
|
56
|
+
@media (pointer: coarse) { #grid-btn { display: none !important; } }
|
|
55
57
|
|
|
56
58
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
57
59
|
html, body { height: 100%; background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; overflow: hidden; }
|
|
@@ -334,7 +336,7 @@
|
|
|
334
336
|
<body>
|
|
335
337
|
|
|
336
338
|
<div id="top-bar">
|
|
337
|
-
<button class="top-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)" style="font-size:1.05rem;padding:0 0.35rem;color:var(--blue2)">▦</button>
|
|
339
|
+
<button id="grid-btn" class="top-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)" style="font-size:1.05rem;padding:0 0.35rem;color:var(--blue2)">▦</button>
|
|
338
340
|
<div id="title-group">
|
|
339
341
|
<div id="session-title">加载中...</div>
|
|
340
342
|
<div id="session-cwd"></div>
|
|
@@ -425,6 +427,7 @@
|
|
|
425
427
|
<button class="sk sk-claude" onclick="openResumeSheet()">恢复</button>
|
|
426
428
|
<button class="sk sk-claude" onclick="doClear()">清除</button>
|
|
427
429
|
<button class="sk sk-claude" onclick="openSecretsSheet()" style="color:var(--blue2)">🔑 密钥</button>
|
|
430
|
+
<button class="sk sk-claude" onclick="openGuardSheet()" style="color:var(--blue2)">🛡 护栏</button>
|
|
428
431
|
<button class="sk sk-claude" onclick="openGitSheet()" style="color:var(--green)">提交</button>
|
|
429
432
|
<button class="sk sk-claude" onclick="showMemory()">记忆</button>
|
|
430
433
|
<button class="sk sk-claude" onclick="openVaSheet()" style="color:var(--blue2)">语音</button>
|
|
@@ -616,6 +619,63 @@
|
|
|
616
619
|
</div>
|
|
617
620
|
</div>
|
|
618
621
|
|
|
622
|
+
<!-- 隐私护栏(Layer C,去标识化):会话级,绑定本会话 cwd -->
|
|
623
|
+
<div id="guard-sheet" class="action-sheet">
|
|
624
|
+
<div class="action-sheet-backdrop" onclick="closeGuardSheet()"></div>
|
|
625
|
+
<div class="action-sheet-box" style="max-height:85vh;display:flex;flex-direction:column">
|
|
626
|
+
<div class="action-sheet-title">🛡 隐私护栏 · 本会话</div>
|
|
627
|
+
<div style="font-size:0.68rem;color:var(--muted);padding:0 0.2rem 0.5rem;line-height:1.55">
|
|
628
|
+
开启后,你<b>贴进对话框</b>的提示词/数据里的敏感实体,会在<b>发给模型之前</b>换成占位符
|
|
629
|
+
<code>【类型_N】</code>(模型/网关只看到假名),答案里再<b>自动还原</b>成真名,映射表只留本机。
|
|
630
|
+
<br><span style="color:var(--yellow)">⚠️ 只保护贴进对话框的内容;若让 AI 用工具去读原始文件,那份数据 myhi 拦不到。</span>
|
|
631
|
+
</div>
|
|
632
|
+
<!-- 开关 + 档位 -->
|
|
633
|
+
<div style="display:flex;align-items:center;gap:0.6rem;padding:0.4rem 0.2rem;border-top:1px solid var(--surface2)">
|
|
634
|
+
<label style="display:flex;align-items:center;gap:0.4rem;font-size:0.82rem;cursor:pointer">
|
|
635
|
+
<input type="checkbox" id="guard-enabled" onchange="saveGuardConfig()" style="width:1.1rem;height:1.1rem"> 启用护栏
|
|
636
|
+
</label>
|
|
637
|
+
<select id="guard-level" class="slash-inp" onchange="saveGuardConfig()" style="flex:1;max-width:180px">
|
|
638
|
+
<option value="high">仅高危(证件/卡号/手机…)</option>
|
|
639
|
+
<option value="standard">标准(+ 主体词典)</option>
|
|
640
|
+
<option value="strict">严格(+ 金额)</option>
|
|
641
|
+
</select>
|
|
642
|
+
<a onclick="openGuardAudit()" style="color:var(--blue2);cursor:pointer;font-size:0.78rem;white-space:nowrap">📋 审计</a>
|
|
643
|
+
</div>
|
|
644
|
+
<!-- 实体词典 -->
|
|
645
|
+
<div style="font-size:0.66rem;color:var(--muted);padding:0.4rem 0.2rem 0.3rem;line-height:1.5;border-top:1px solid var(--surface2)">
|
|
646
|
+
<b>主体词典</b>——录入"我方主体/关联公司/关键人"名单(标准/严格档生效),命中即脱敏。
|
|
647
|
+
<a onclick="toggleBulkGuard()" id="guard-bulk-toggle" style="color:var(--blue2);cursor:pointer;white-space:nowrap">批量 ⇄</a>
|
|
648
|
+
</div>
|
|
649
|
+
<!-- 列表模式 -->
|
|
650
|
+
<div id="guard-mode-list" style="flex:1;display:flex;flex-direction:column;min-height:0">
|
|
651
|
+
<div id="guard-list" style="flex:1;overflow-y:auto;padding:0.2rem 0;font-size:0.8rem;scrollbar-width:thin;min-height:4rem"></div>
|
|
652
|
+
<div style="display:flex;gap:0.4rem;padding:0.3rem 0.2rem;border-top:1px solid var(--surface2)">
|
|
653
|
+
<input id="guard-ent-name" class="slash-inp" placeholder="实体名 如 阿里巴巴集团" autocomplete="off" style="flex:1">
|
|
654
|
+
<input id="guard-ent-type" class="slash-inp" placeholder="类型 主体" autocomplete="off" style="max-width:88px">
|
|
655
|
+
</div>
|
|
656
|
+
<button class="action-sheet-cancel" style="background:var(--blue2);color:#fff;font-weight:600;margin:0.4rem 0" onclick="addGuardEntity()">添加实体</button>
|
|
657
|
+
</div>
|
|
658
|
+
<!-- 批量模式 -->
|
|
659
|
+
<div id="guard-mode-bulk" style="display:none;flex:1;flex-direction:column;min-height:0">
|
|
660
|
+
<div style="font-size:0.66rem;color:var(--muted);padding:0 0.2rem 0.3rem;line-height:1.5">
|
|
661
|
+
每行一条:<code>名称</code> 或 <code>名称|类型</code>;<code>#</code> 开头忽略。非破坏性(不删漏列的,删用列表)。
|
|
662
|
+
</div>
|
|
663
|
+
<textarea id="guard-bulk-text" spellcheck="false" placeholder="阿里巴巴集团|机构 张三丰|人名 某某项目公司" style="flex:1;min-height:8rem;width:100%;box-sizing:border-box;font-family:monospace;font-size:0.78rem;background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:0.5rem;resize:vertical"></textarea>
|
|
664
|
+
<button class="action-sheet-cancel" style="background:var(--blue2);color:#fff;font-weight:600;margin:0.4rem 0" onclick="saveBulkGuard()">批量保存</button>
|
|
665
|
+
</div>
|
|
666
|
+
<!-- 审计留痕:每轮脱敏记录(发给模型的脱敏后全文,可导出取证) -->
|
|
667
|
+
<div id="guard-mode-audit" style="display:none;flex:1;flex-direction:column;min-height:0">
|
|
668
|
+
<div style="display:flex;align-items:center;gap:0.5rem;padding:0 0.2rem 0.3rem">
|
|
669
|
+
<div style="flex:1;font-size:0.66rem;color:var(--muted);line-height:1.5">每轮把敏感实体脱敏后发给模型的记录(<b>只含脱敏后假名文本、不含真值</b>,可导出给合规方取证)。</div>
|
|
670
|
+
<button class="sk sk-claude" style="padding:0.1rem 0.6rem" onclick="exportGuardAudit()">导出</button>
|
|
671
|
+
<a onclick="setGuardMode('list')" style="color:var(--blue2);cursor:pointer;font-size:0.78rem">← 返回</a>
|
|
672
|
+
</div>
|
|
673
|
+
<div id="guard-audit-list" style="flex:1;overflow-y:auto;padding:0.2rem 0;font-size:0.78rem;scrollbar-width:thin;min-height:6rem"></div>
|
|
674
|
+
</div>
|
|
675
|
+
<button class="action-sheet-cancel" onclick="closeGuardSheet()">关闭</button>
|
|
676
|
+
</div>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
619
679
|
<div id="memory-sheet" class="action-sheet">
|
|
620
680
|
<div class="action-sheet-backdrop" onclick="closeMemorySheet()"></div>
|
|
621
681
|
<div class="action-sheet-box" style="height:90vh;max-height:90vh;display:flex;flex-direction:column">
|
|
@@ -3128,6 +3188,146 @@
|
|
|
3128
3188
|
} catch {}
|
|
3129
3189
|
};
|
|
3130
3190
|
|
|
3191
|
+
// ── 隐私护栏(Layer C,去标识化):会话级自助维护 ──────────────
|
|
3192
|
+
window.openGuardSheet = function() {
|
|
3193
|
+
document.getElementById('guard-sheet').classList.add('open');
|
|
3194
|
+
setGuardMode('list');
|
|
3195
|
+
loadGuard();
|
|
3196
|
+
};
|
|
3197
|
+
window.closeGuardSheet = function() {
|
|
3198
|
+
document.getElementById('guard-sheet').classList.remove('open');
|
|
3199
|
+
};
|
|
3200
|
+
window.setGuardMode = function(mode) {
|
|
3201
|
+
document.getElementById('guard-mode-list').style.display = mode === 'list' ? 'flex' : 'none';
|
|
3202
|
+
document.getElementById('guard-mode-bulk').style.display = mode === 'bulk' ? 'flex' : 'none';
|
|
3203
|
+
document.getElementById('guard-mode-audit').style.display = mode === 'audit' ? 'flex' : 'none';
|
|
3204
|
+
document.getElementById('guard-bulk-toggle').textContent = mode === 'bulk' ? '列表 ⇄' : '批量 ⇄';
|
|
3205
|
+
};
|
|
3206
|
+
window.toggleBulkGuard = function() {
|
|
3207
|
+
const goingBulk = document.getElementById('guard-mode-bulk').style.display === 'none';
|
|
3208
|
+
setGuardMode(goingBulk ? 'bulk' : 'list');
|
|
3209
|
+
};
|
|
3210
|
+
async function loadGuard() {
|
|
3211
|
+
const box = document.getElementById('guard-list');
|
|
3212
|
+
box.innerHTML = '<div style="text-align:center;color:var(--muted);padding:1rem 0">加载中...</div>';
|
|
3213
|
+
try {
|
|
3214
|
+
const r = await fetch(`/api/session/${SESSION_ID}/guard`);
|
|
3215
|
+
if (!r.ok) { box.innerHTML = '<div style="color:var(--muted);padding:0.5rem">读取失败</div>'; return; }
|
|
3216
|
+
const d = await r.json();
|
|
3217
|
+
document.getElementById('guard-enabled').checked = !!d.enabled;
|
|
3218
|
+
document.getElementById('guard-level').value = d.level || 'standard';
|
|
3219
|
+
const ents = d.entities || [];
|
|
3220
|
+
if (!ents.length) {
|
|
3221
|
+
box.innerHTML = '<div style="color:var(--muted);font-size:0.75rem;padding:0.3rem">(暂无主体,录入"我方主体/关联公司/关键人"名单)</div>';
|
|
3222
|
+
} else {
|
|
3223
|
+
box.innerHTML = ents.map(e => `<div style="display:flex;align-items:center;gap:0.5rem;padding:0.3rem 0.2rem;border-bottom:1px solid var(--surface2)">
|
|
3224
|
+
<span style="flex:1">${escHtml(e.name)}</span>
|
|
3225
|
+
<span style="color:var(--muted);font-size:0.7rem">【${escHtml(e.type || '主体')}】</span>
|
|
3226
|
+
<button class="sk sk-claude" style="padding:0.05rem 0.5rem" onclick="delGuardEntity('${escHtml(e.name)}')">删</button>
|
|
3227
|
+
</div>`).join('');
|
|
3228
|
+
}
|
|
3229
|
+
} catch { box.innerHTML = '<div style="color:var(--muted);padding:0.5rem">连接失败</div>'; }
|
|
3230
|
+
}
|
|
3231
|
+
window.saveGuardConfig = async function() {
|
|
3232
|
+
const enabled = document.getElementById('guard-enabled').checked;
|
|
3233
|
+
const level = document.getElementById('guard-level').value;
|
|
3234
|
+
try {
|
|
3235
|
+
await fetch(`/api/session/${SESSION_ID}/guard`, {
|
|
3236
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
3237
|
+
body: JSON.stringify({ enabled, level }),
|
|
3238
|
+
});
|
|
3239
|
+
addStatusMessage(enabled ? `🛡 护栏已开启(${level}),下一条消息起对模型脱敏` : '🛡 护栏已关闭');
|
|
3240
|
+
} catch { addStatusMessage('连接失败'); }
|
|
3241
|
+
};
|
|
3242
|
+
window.addGuardEntity = async function() {
|
|
3243
|
+
const name = document.getElementById('guard-ent-name').value.trim();
|
|
3244
|
+
const type = document.getElementById('guard-ent-type').value.trim();
|
|
3245
|
+
if (!name) { addStatusMessage('实体名要填'); return; }
|
|
3246
|
+
try {
|
|
3247
|
+
const r = await fetch(`/api/session/${SESSION_ID}/guard/entity`, {
|
|
3248
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
3249
|
+
body: JSON.stringify({ name, type }),
|
|
3250
|
+
});
|
|
3251
|
+
const d = await r.json();
|
|
3252
|
+
if (d.ok) {
|
|
3253
|
+
document.getElementById('guard-ent-name').value = '';
|
|
3254
|
+
document.getElementById('guard-ent-type').value = '';
|
|
3255
|
+
loadGuard();
|
|
3256
|
+
} else { addStatusMessage(d.error || '添加失败'); }
|
|
3257
|
+
} catch { addStatusMessage('连接失败'); }
|
|
3258
|
+
};
|
|
3259
|
+
window.delGuardEntity = async function(name) {
|
|
3260
|
+
try {
|
|
3261
|
+
await fetch(`/api/session/${SESSION_ID}/guard/entity`, {
|
|
3262
|
+
method: 'DELETE', headers: { 'Content-Type': 'application/json' },
|
|
3263
|
+
body: JSON.stringify({ name }),
|
|
3264
|
+
});
|
|
3265
|
+
loadGuard();
|
|
3266
|
+
} catch {}
|
|
3267
|
+
};
|
|
3268
|
+
window.saveBulkGuard = async function() {
|
|
3269
|
+
const text = document.getElementById('guard-bulk-text').value;
|
|
3270
|
+
try {
|
|
3271
|
+
const r = await fetch(`/api/session/${SESSION_ID}/guard/bulk`, {
|
|
3272
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
3273
|
+
body: JSON.stringify({ text }),
|
|
3274
|
+
});
|
|
3275
|
+
const d = await r.json();
|
|
3276
|
+
if (d.ok) {
|
|
3277
|
+
addStatusMessage(`批量保存:录入 ${d.count} 条${d.errors && d.errors.length ? ',跳过 ' + d.errors.length + ' 行' : ''}`);
|
|
3278
|
+
document.getElementById('guard-bulk-text').value = '';
|
|
3279
|
+
setGuardMode('list');
|
|
3280
|
+
loadGuard();
|
|
3281
|
+
} else { addStatusMessage(d.error || '批量保存失败'); }
|
|
3282
|
+
} catch { addStatusMessage('连接失败'); }
|
|
3283
|
+
};
|
|
3284
|
+
let _lastAudit = [];
|
|
3285
|
+
window.openGuardAudit = async function() {
|
|
3286
|
+
setGuardMode('audit');
|
|
3287
|
+
const box = document.getElementById('guard-audit-list');
|
|
3288
|
+
box.innerHTML = '<div style="text-align:center;color:var(--muted);padding:1rem 0">加载中...</div>';
|
|
3289
|
+
try {
|
|
3290
|
+
const r = await fetch(`/api/session/${SESSION_ID}/guard/audit`);
|
|
3291
|
+
const d = await r.json();
|
|
3292
|
+
_lastAudit = d.entries || [];
|
|
3293
|
+
if (!_lastAudit.length) {
|
|
3294
|
+
box.innerHTML = '<div style="color:var(--muted);font-size:0.75rem;padding:0.5rem">(暂无记录——开启护栏后有敏感信息的轮次会在此留痕)</div>';
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
box.innerHTML = _lastAudit.slice().reverse().map(e => {
|
|
3298
|
+
const t = new Date(e.ts).toLocaleString();
|
|
3299
|
+
const types = Object.entries(e.byType || {}).map(([k, v]) => `${escHtml(k)}×${v}`).join(' ');
|
|
3300
|
+
return `<div style="padding:0.4rem 0.2rem;border-bottom:1px solid var(--surface2)">
|
|
3301
|
+
<div style="display:flex;gap:0.5rem;align-items:baseline;flex-wrap:wrap">
|
|
3302
|
+
<span style="color:var(--muted);font-size:0.7rem">${t}</span>
|
|
3303
|
+
<span style="color:var(--blue2)">脱敏 ${e.maskedCount} 处</span>
|
|
3304
|
+
<span style="color:var(--muted);font-size:0.72rem">${types}</span>
|
|
3305
|
+
</div>
|
|
3306
|
+
<details style="margin-top:0.25rem"><summary style="cursor:pointer;color:var(--muted);font-size:0.72rem">发给模型的脱敏后内容</summary>
|
|
3307
|
+
<pre style="white-space:pre-wrap;word-break:break-all;background:var(--surface2);border-radius:6px;padding:0.4rem;margin:0.3rem 0 0;font-size:0.72rem;max-height:12rem;overflow:auto">${escHtml(e.sentToModel || '')}</pre>
|
|
3308
|
+
</details>
|
|
3309
|
+
</div>`;
|
|
3310
|
+
}).join('');
|
|
3311
|
+
} catch { box.innerHTML = '<div style="color:var(--muted);padding:0.5rem">连接失败</div>'; }
|
|
3312
|
+
};
|
|
3313
|
+
window.exportGuardAudit = function() {
|
|
3314
|
+
if (!_lastAudit.length) { addStatusMessage('暂无审计记录可导出'); return; }
|
|
3315
|
+
const lines = ['# myhi 隐私护栏 · 审计留痕', '# 只含发给模型的脱敏后假名文本,不含真值', ''];
|
|
3316
|
+
for (const e of _lastAudit) {
|
|
3317
|
+
const types = Object.entries(e.byType || {}).map(([k, v]) => `${k}×${v}`).join(' ');
|
|
3318
|
+
lines.push(`【${new Date(e.ts).toLocaleString()}】档位=${e.level || '-'} 脱敏 ${e.maskedCount} 处 ${types}`);
|
|
3319
|
+
lines.push('--- 发给模型 ---');
|
|
3320
|
+
lines.push(e.sentToModel || '');
|
|
3321
|
+
lines.push('');
|
|
3322
|
+
}
|
|
3323
|
+
const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' });
|
|
3324
|
+
const a = document.createElement('a');
|
|
3325
|
+
a.href = URL.createObjectURL(blob);
|
|
3326
|
+
a.download = `myhi-隐私审计-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.txt`;
|
|
3327
|
+
a.click();
|
|
3328
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
|
|
3329
|
+
};
|
|
3330
|
+
|
|
3131
3331
|
// ── 会话命名 ──────────────────────────────────
|
|
3132
3332
|
let _slashCmd = null;
|
|
3133
3333
|
const _slashTitles = { '/plan': '计划模式', '/compact': '压缩上下文', '/rename': '重命名会话' };
|
package/dist/grid.html
CHANGED
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
<label style="font-size:0.72rem;color:var(--muted)">启动命令(可选,终端类型用)</label>
|
|
111
111
|
<input id="nt-cmd" placeholder="留空仅打开 shell" autocomplete="off" style="width:100%;box-sizing:border-box;margin:0.2rem 0 0.6rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:0.45rem 0.6rem;font-size:0.82rem;font-family:monospace">
|
|
112
112
|
<label style="font-size:0.72rem;color:var(--muted)">工作目录(可选)</label>
|
|
113
|
-
<input id="nt-cwd" placeholder="
|
|
113
|
+
<input id="nt-cwd" placeholder="留空 → 建在 默认目录/任务名 下(每个任务独立目录)" autocomplete="off" style="width:100%;box-sizing:border-box;margin:0.2rem 0 0.4rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:0.45rem 0.6rem;font-size:0.82rem">
|
|
114
114
|
<div id="nt-recent" style="display:flex;flex-wrap:wrap;gap:0.3rem;margin-bottom:0.7rem"></div>
|
|
115
115
|
<div style="display:flex;gap:0.5rem">
|
|
116
116
|
<button onclick="closeNewTask()" style="flex:1;background:var(--bg);border:1px solid var(--border);color:var(--muted);border-radius:8px;padding:0.5rem;cursor:pointer">取消</button>
|
|
@@ -348,8 +348,10 @@
|
|
|
348
348
|
if (res && res.ok && res.session) { closeNewTask(); refreshSessions(true).then(()=>addPane(res.session.id)); }
|
|
349
349
|
else alert('创建失败:' + ((res && res.error) || '未知错误'));
|
|
350
350
|
};
|
|
351
|
-
|
|
352
|
-
|
|
351
|
+
// 工作目录留空时:让服务端按任务名建在「默认目录/任务名」下,避免多任务全堆在同一默认目录
|
|
352
|
+
const nestByTitle = !cwd;
|
|
353
|
+
if (preset.type === 'agent') socket.emit('create-agent', { title, cwd, nestByTitle }, done);
|
|
354
|
+
else socket.emit('create', { title, initCmd: cmd, cwd, nestByTitle }, done);
|
|
353
355
|
};
|
|
354
356
|
document.getElementById('nt-presets').addEventListener('click', (e) => {
|
|
355
357
|
const b = e.target.closest('[data-i]'); if (!b) return;
|
package/dist/index.html
CHANGED
|
@@ -428,6 +428,8 @@
|
|
|
428
428
|
#update-btn { background: var(--yellow); color: #000; border: none; border-radius: 6px; padding: 0.35rem 0.75rem; font-size: 0.75rem; font-weight: 600; cursor: pointer; white-space: nowrap; }
|
|
429
429
|
#update-btn:active { opacity: 0.7; }
|
|
430
430
|
#update-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
431
|
+
/* 分屏是 PC 大屏功能,手机端(触屏)隐藏入口 */
|
|
432
|
+
@media (pointer: coarse) { #grid-btn { display: none !important; } }
|
|
431
433
|
</style>
|
|
432
434
|
</head>
|
|
433
435
|
<body>
|