@wendongfly/myhi 1.3.143 → 1.3.144
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/dist/chat.html +300 -4
- package/dist/index.js +4 -4
- package/dist/index.min.js +136 -136
- package/package.json +1 -1
package/dist/chat.html
CHANGED
|
@@ -297,6 +297,21 @@
|
|
|
297
297
|
.fp-diff-actions .fp-btn { flex: 1; text-align: center; padding: 0.45rem; }
|
|
298
298
|
.fp-btn-primary { background: var(--green2); border-color: #2ea04380; }
|
|
299
299
|
.fp-btn-primary:active { background: var(--green2); }
|
|
300
|
+
|
|
301
|
+
/* ── 文件在线编辑(聊天里/文件列表里点文本文件路径)── */
|
|
302
|
+
.file-link { color: var(--blue); text-decoration: underline; cursor: pointer; text-underline-offset: 2px; }
|
|
303
|
+
.file-link:active { opacity: 0.7; }
|
|
304
|
+
#fedit-ta {
|
|
305
|
+
width: 100%; flex: 1; min-height: 0; box-sizing: border-box; display: none;
|
|
306
|
+
background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px;
|
|
307
|
+
padding: 0.6rem; font-family: 'SF Mono', Consolas, monospace; font-size: 0.78rem; line-height: 1.55;
|
|
308
|
+
resize: none; outline: none;
|
|
309
|
+
}
|
|
310
|
+
#fedit-ta:focus { border-color: var(--purple); }
|
|
311
|
+
/* 预览复用记忆面板的 .md 渲染样式,只解开它自带的 max-height/隐藏 */
|
|
312
|
+
#fedit-preview { display: none; max-height: none; flex: 1; min-height: 0; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); }
|
|
313
|
+
.fedit-tab { flex-shrink: 0; }
|
|
314
|
+
.fedit-tab.on { border-color: var(--purple); background: var(--purple); color: #fff; }
|
|
300
315
|
.fp-btn-warn { background: var(--yellow2); border-color: #d2992280; }
|
|
301
316
|
.fp-btn-warn:active { background: var(--yellow2); }
|
|
302
317
|
|
|
@@ -815,6 +830,34 @@
|
|
|
815
830
|
</div>
|
|
816
831
|
</div>
|
|
817
832
|
|
|
833
|
+
<!-- 文件在线编辑(md/代码/配置等文本文件) -->
|
|
834
|
+
<div id="fedit-sheet" class="action-sheet">
|
|
835
|
+
<div class="action-sheet-backdrop" onclick="closeFileEditor()"></div>
|
|
836
|
+
<div class="action-sheet-box" style="height:92vh;max-height:92vh;display:flex;flex-direction:column">
|
|
837
|
+
<div class="action-sheet-title" style="display:flex;align-items:center;gap:0.5rem">
|
|
838
|
+
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">✏️ <span id="fedit-title">文件</span></span>
|
|
839
|
+
<span id="fedit-state" style="font-size:0.64rem;color:var(--muted);flex-shrink:0;font-weight:400"></span>
|
|
840
|
+
</div>
|
|
841
|
+
<div id="fedit-path" style="font-size:0.66rem;color:var(--muted);padding:0 0.2rem 0.4rem;word-break:break-all"></div>
|
|
842
|
+
<div style="flex:1;min-height:0;display:flex;flex-direction:column;gap:0.4rem;padding:0 0.2rem">
|
|
843
|
+
<div id="fedit-loading" style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:0.82rem">加载中...</div>
|
|
844
|
+
<textarea id="fedit-ta" spellcheck="false"></textarea>
|
|
845
|
+
<div id="fedit-preview" class="mem-file-body md"></div>
|
|
846
|
+
</div>
|
|
847
|
+
<div style="display:flex;gap:0.4rem;padding:0.5rem 0.2rem 0;align-items:center">
|
|
848
|
+
<button class="fp-btn fedit-tab" id="fedit-tab-edit" onclick="feditSetMode('edit')">✏️ 编辑</button>
|
|
849
|
+
<button class="fp-btn fedit-tab" id="fedit-tab-preview" onclick="feditSetMode('preview')" style="display:none">👁 预览</button>
|
|
850
|
+
<span style="flex:1"></span>
|
|
851
|
+
<button class="fp-btn" onclick="feditReload()" title="放弃本地修改,重新读磁盘最新内容">🔄 重读</button>
|
|
852
|
+
<button class="fp-btn" onclick="closeFileEditor()">关闭</button>
|
|
853
|
+
</div>
|
|
854
|
+
<div style="display:flex;gap:0.4rem;padding:0.45rem 0.2rem 0.2rem">
|
|
855
|
+
<button id="fedit-save" style="flex:2;padding:0.5rem;border:none;border-radius:8px;background:var(--blue2);color:#fff;cursor:pointer;font-weight:600;font-size:0.8rem">💾 保存</button>
|
|
856
|
+
<button id="fedit-save-tell" style="flex:3;padding:0.5rem;border:1px solid var(--purple);border-radius:8px;background:var(--bg);color:var(--purple);cursor:pointer;font-weight:600;font-size:0.8rem" title="保存后发一条消息让 AI 重新读取这个文件">📨 保存并告诉 AI</button>
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
</div>
|
|
860
|
+
|
|
818
861
|
<!-- 自定义 Slash Commands 弹窗 -->
|
|
819
862
|
<div id="cmd-sheet" class="action-sheet">
|
|
820
863
|
<div class="action-sheet-backdrop" onclick="closeCmdSheet()"></div>
|
|
@@ -1130,6 +1173,97 @@
|
|
|
1130
1173
|
|
|
1131
1174
|
// ── 简易 Markdown ──────────────────────────────
|
|
1132
1175
|
function escHtml(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
1176
|
+
// 用于 onclick="...('${x}')" 这类属性位:escHtml 不管引号,单引号会把属性截断
|
|
1177
|
+
function escAttr(s) { return escHtml(String(s)).replace(/'/g, ''').replace(/"/g, '"'); }
|
|
1178
|
+
|
|
1179
|
+
// ── 可在线编辑的文本文件 ──────────────────────────
|
|
1180
|
+
// 白名单与服务端 EDITABLE_EXTS 一致(改一处记得改 src/server.js)
|
|
1181
|
+
const FEDIT_EXT = new Set([
|
|
1182
|
+
'md','markdown','txt','log','json','jsonc','json5',
|
|
1183
|
+
'yml','yaml','toml','ini','cfg','conf','env','properties','editorconfig',
|
|
1184
|
+
'csv','tsv','xml','svg','html','htm','css','scss','less',
|
|
1185
|
+
'js','mjs','cjs','jsx','ts','mts','cts','tsx','vue','svelte',
|
|
1186
|
+
'py','pyi','sh','bash','zsh','ps1','psm1','bat','cmd',
|
|
1187
|
+
'sql','java','kt','kts','groovy','gradle',
|
|
1188
|
+
'c','h','cpp','hpp','cc','cxx','cs','go','rs','rb','php','swift','lua','dart',
|
|
1189
|
+
'proto','graphql','cmake','dockerfile','gitignore',
|
|
1190
|
+
]);
|
|
1191
|
+
window.isEditableFile = function(name) {
|
|
1192
|
+
const m = String(name).toLowerCase().match(/\.([a-z0-9]+)$/);
|
|
1193
|
+
return !!(m && FEDIT_EXT.has(m[1]));
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
// 聊天流里 AI 提到的文件路径("已写入 docs/报告.md")→ 变成可点的链接,点了直接开编辑器。
|
|
1197
|
+
// 只认"一段路径字符收尾于白名单扩展名",中文路径、Windows 反斜杠、盘符绝对路径都覆盖。
|
|
1198
|
+
const _PATH_CHARS = "[\\w.\\-\\u4e00-\\u9fa5\\\\/]";
|
|
1199
|
+
const _PATH_RE = new RegExp(_PATH_CHARS + "+\\.(" + [...FEDIT_EXT].join('|') + ")", "gi");
|
|
1200
|
+
// 从一段纯文本里找出可点的路径。抽成纯函数:linkify 的 DOM 遍历用它,离线单测也直接 eval 它。
|
|
1201
|
+
// 命中前一个字符的特判:是 'D:' 盘符 → 把盘符并进链接;是 'https:' / '@'(邮箱)/ '/'(URL 尾)→ 跳过。
|
|
1202
|
+
function matchPathsInText(s) {
|
|
1203
|
+
_PATH_RE.lastIndex = 0;
|
|
1204
|
+
const out = [];
|
|
1205
|
+
let m;
|
|
1206
|
+
while ((m = _PATH_RE.exec(s))) {
|
|
1207
|
+
let start = m.index, text = m[0];
|
|
1208
|
+
const prev = start > 0 ? s[start - 1] : '';
|
|
1209
|
+
if (prev === ':') {
|
|
1210
|
+
// 'D:' 盘符(且盘符字母前不是别的字母——否则 https: 的 's:' 会误判)→ 并进链接
|
|
1211
|
+
const isDrive = start >= 2 && /[A-Za-z]:$/.test(s.slice(start - 2, start)) &&
|
|
1212
|
+
!(start >= 3 && /\w/.test(s[start - 3]));
|
|
1213
|
+
if (isDrive) {
|
|
1214
|
+
start -= 2;
|
|
1215
|
+
text = s.slice(start, m.index + m[0].length);
|
|
1216
|
+
} else continue;
|
|
1217
|
+
} else if (prev === '@' || prev === '/') continue;
|
|
1218
|
+
out.push({ start, text });
|
|
1219
|
+
}
|
|
1220
|
+
return out;
|
|
1221
|
+
}
|
|
1222
|
+
function linkifyFilePathsHtml(html) {
|
|
1223
|
+
let box;
|
|
1224
|
+
try { box = document.createElement('div'); box.innerHTML = html; } catch { return html; }
|
|
1225
|
+
const walker = document.createTreeWalker(box, NodeFilter.SHOW_TEXT, {
|
|
1226
|
+
acceptNode(n) {
|
|
1227
|
+
if (!n.nodeValue || !_PATH_RE.test(n.nodeValue)) return NodeFilter.FILTER_REJECT;
|
|
1228
|
+
_PATH_RE.lastIndex = 0;
|
|
1229
|
+
// 代码块和已有链接里不放链接(代码示例里的路径多是演示,点了也是 404)
|
|
1230
|
+
let el = n.parentNode;
|
|
1231
|
+
while (el && el !== box) {
|
|
1232
|
+
if (el.tagName === 'PRE' || el.tagName === 'A') return NodeFilter.FILTER_REJECT;
|
|
1233
|
+
el = el.parentNode;
|
|
1234
|
+
}
|
|
1235
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
1236
|
+
},
|
|
1237
|
+
});
|
|
1238
|
+
const texts = [];
|
|
1239
|
+
while (walker.nextNode()) texts.push(walker.currentNode);
|
|
1240
|
+
for (const node of texts) {
|
|
1241
|
+
const s = node.nodeValue;
|
|
1242
|
+
const hits = matchPathsInText(s);
|
|
1243
|
+
if (!hits.length) continue;
|
|
1244
|
+
const frag = document.createDocumentFragment();
|
|
1245
|
+
let last = 0;
|
|
1246
|
+
for (const h of hits) {
|
|
1247
|
+
if (h.start > last) frag.appendChild(document.createTextNode(s.slice(last, h.start)));
|
|
1248
|
+
const a = document.createElement('span');
|
|
1249
|
+
a.className = 'file-link';
|
|
1250
|
+
a.textContent = h.text;
|
|
1251
|
+
a.dataset.path = h.text;
|
|
1252
|
+
frag.appendChild(a);
|
|
1253
|
+
last = h.start + h.text.length;
|
|
1254
|
+
}
|
|
1255
|
+
if (last < s.length) frag.appendChild(document.createTextNode(s.slice(last)));
|
|
1256
|
+
node.parentNode.replaceChild(frag, node);
|
|
1257
|
+
}
|
|
1258
|
+
return box.innerHTML;
|
|
1259
|
+
}
|
|
1260
|
+
// 聊天流/记忆面板里所有 .file-link 点击统一走这里(渲染时委托,动态节点免绑)
|
|
1261
|
+
document.addEventListener('click', (e) => {
|
|
1262
|
+
const el = e.target && e.target.closest ? e.target.closest('.file-link') : null;
|
|
1263
|
+
if (!el) return;
|
|
1264
|
+
e.preventDefault();
|
|
1265
|
+
openFileEditor(el.dataset.path || '');
|
|
1266
|
+
});
|
|
1133
1267
|
|
|
1134
1268
|
function renderMarkdown(text) {
|
|
1135
1269
|
const lines = text.split('\n');
|
|
@@ -1195,7 +1329,8 @@
|
|
|
1195
1329
|
if (inCode) html += `<pre><code>${escHtml(codeLines.join('\n'))}</code></pre>`;
|
|
1196
1330
|
// 包裹连续 li 为 ul
|
|
1197
1331
|
html = html.replace(/(<li>[\s\S]*?<\/li>)(?![\s\S]*?<li>)/g, (m) => `<ul>${m}</ul>`);
|
|
1198
|
-
|
|
1332
|
+
// AI 提到的文件路径 → 可点击的编辑链接
|
|
1333
|
+
try { return linkifyFilePathsHtml(html); } catch { return html; }
|
|
1199
1334
|
}
|
|
1200
1335
|
|
|
1201
1336
|
// ── 块分类器 ──────────────────────────────────
|
|
@@ -2589,11 +2724,13 @@
|
|
|
2589
2724
|
if (e.isDir) {
|
|
2590
2725
|
html += `<div class="fp-item" onclick="fpNav('${escHtml(rel)}')"><span class="fp-icon">📁</span><span class="fp-name">${escHtml(e.name)}</span></div>`;
|
|
2591
2726
|
} else {
|
|
2592
|
-
|
|
2593
|
-
|
|
2727
|
+
const editable = isEditableFile(e.name);
|
|
2728
|
+
html += `<div class="fp-item" ${editable ? `style="cursor:pointer" onclick="openFileEditor('${escAttr(rel)}')" title="点击在线编辑"` : ''}>
|
|
2729
|
+
<span class="fp-icon">${editable ? '📝' : '📄'}</span>
|
|
2594
2730
|
<span class="fp-name">${escHtml(e.name)}</span>
|
|
2595
2731
|
<span class="fp-size">${fpFormatSize(e.size)}</span>
|
|
2596
|
-
|
|
2732
|
+
${editable ? `<span class="fp-dl" onclick="event.stopPropagation();openFileEditor('${escAttr(rel)}')" title="在线编辑">编辑</span>` : ''}
|
|
2733
|
+
<a class="fp-dl" href="/api/files/${SESSION_ID}/download?path=${encodeURIComponent(rel)}" download title="下载" onclick="event.stopPropagation()">下载</a>
|
|
2597
2734
|
</div>`;
|
|
2598
2735
|
}
|
|
2599
2736
|
}
|
|
@@ -2662,6 +2799,165 @@
|
|
|
2662
2799
|
}
|
|
2663
2800
|
});
|
|
2664
2801
|
|
|
2802
|
+
// ── 文件在线编辑器 ──────────────────────────────
|
|
2803
|
+
// AI 生成的 md/代码/配置直接在网页里看+改+存回,不用下载→本机改→传回。
|
|
2804
|
+
// 入口:①文件面板点文本文件/「编辑」 ②聊天流里 AI 提到的路径(file-link)。
|
|
2805
|
+
// 冲突保护:打开时记 mtime,保存时文件被(多半是 AI)改过 → 409 → 问用户覆盖还是重读。
|
|
2806
|
+
let _fedit = null; // { path, name, mtime, md, mode, loaded, dirty }
|
|
2807
|
+
|
|
2808
|
+
// 路径归一:剥掉包裹引号/标点,反斜杠转正斜杠;会话 cwd 下的绝对路径转成相对
|
|
2809
|
+
function feditNormPath(p) {
|
|
2810
|
+
p = p.replace(/^[\s"'`<(("']+|[\s"'`>))"'。,,;;]+$/g, '');
|
|
2811
|
+
if (!p) return '';
|
|
2812
|
+
p = p.replace(/\\/g, '/');
|
|
2813
|
+
if (/^[A-Za-z]:\//.test(p) || p.startsWith('/')) {
|
|
2814
|
+
const cwd = String(currentSession?.cwd || '').replace(/\\/g, '/').replace(/\/+$/, '');
|
|
2815
|
+
if (cwd && (cwd + '/').toLowerCase() !== '/' && p.toLowerCase().startsWith((cwd + '/').toLowerCase())) {
|
|
2816
|
+
return p.slice(cwd.length + 1);
|
|
2817
|
+
}
|
|
2818
|
+
return p; // cwd 外的绝对路径交给服务端判越界
|
|
2819
|
+
}
|
|
2820
|
+
return p.replace(/^\.\/+/, '');
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
function feditSetState(t) { const el = document.getElementById('fedit-state'); if (el) el.textContent = t || ''; }
|
|
2824
|
+
|
|
2825
|
+
window.openFileEditor = async function(rawPath) {
|
|
2826
|
+
const rel = feditNormPath(String(rawPath || '').trim());
|
|
2827
|
+
if (!rel) { addStatusMessage('路径为空'); return; }
|
|
2828
|
+
if (!isEditableFile(rel)) {
|
|
2829
|
+
addStatusMessage('「' + rel.split('/').pop() + '」不是可编辑的文本类型,请用下载');
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
const sheet = document.getElementById('fedit-sheet');
|
|
2833
|
+
sheet.classList.add('open');
|
|
2834
|
+
document.getElementById('fedit-title').textContent = rel.split('/').pop();
|
|
2835
|
+
document.getElementById('fedit-path').textContent = rel;
|
|
2836
|
+
document.getElementById('fedit-loading').style.display = 'flex';
|
|
2837
|
+
document.getElementById('fedit-loading').textContent = '加载中...';
|
|
2838
|
+
document.getElementById('fedit-ta').style.display = 'none';
|
|
2839
|
+
document.getElementById('fedit-preview').style.display = 'none';
|
|
2840
|
+
feditSetState('');
|
|
2841
|
+
try {
|
|
2842
|
+
const r = await fetch(`/api/files/${SESSION_ID}/content?path=${encodeURIComponent(rel)}`);
|
|
2843
|
+
const d = await r.json().catch(() => ({}));
|
|
2844
|
+
if (!r.ok || !d.ok) {
|
|
2845
|
+
const map = { 404: '文件不存在(可能还没生成,或路径不对)', 403: '路径越界(不在会话目录内)', 413: '文件超过 2MB,请用下载' };
|
|
2846
|
+
addStatusMessage(map[r.status] || (d.error || ('打开失败: HTTP ' + r.status)));
|
|
2847
|
+
sheet.classList.remove('open');
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
_fedit = { path: d.path, name: d.name, mtime: d.mtime, md: !!d.md, mode: 'edit', loaded: d.content, dirty: false };
|
|
2851
|
+
document.getElementById('fedit-ta').value = d.content;
|
|
2852
|
+
document.getElementById('fedit-loading').style.display = 'none';
|
|
2853
|
+
document.getElementById('fedit-tab-preview').style.display = _fedit.md ? '' : 'none';
|
|
2854
|
+
feditSetMode('edit');
|
|
2855
|
+
} catch (e) {
|
|
2856
|
+
addStatusMessage('打开出错: ' + e.message);
|
|
2857
|
+
sheet.classList.remove('open');
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2860
|
+
|
|
2861
|
+
window.feditSetMode = function(mode) {
|
|
2862
|
+
if (!_fedit) return;
|
|
2863
|
+
if (mode === 'preview' && !_fedit.md) mode = 'edit';
|
|
2864
|
+
_fedit.mode = mode;
|
|
2865
|
+
const ta = document.getElementById('fedit-ta');
|
|
2866
|
+
const pv = document.getElementById('fedit-preview');
|
|
2867
|
+
ta.style.display = mode === 'edit' ? 'block' : 'none';
|
|
2868
|
+
pv.style.display = mode === 'preview' ? 'block' : 'none';
|
|
2869
|
+
if (mode === 'preview') pv.innerHTML = renderMarkdown(ta.value);
|
|
2870
|
+
document.getElementById('fedit-tab-edit').classList.toggle('on', mode === 'edit');
|
|
2871
|
+
document.getElementById('fedit-tab-preview').classList.toggle('on', mode === 'preview');
|
|
2872
|
+
};
|
|
2873
|
+
|
|
2874
|
+
document.getElementById('fedit-ta').addEventListener('input', () => {
|
|
2875
|
+
if (_fedit) _fedit.dirty = document.getElementById('fedit-ta').value !== _fedit.loaded;
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2878
|
+
window.closeFileEditor = function() {
|
|
2879
|
+
if (_fedit && _fedit.dirty && !confirm('「' + _fedit.name + '」有未保存的修改,确定丢弃并关闭?')) return;
|
|
2880
|
+
document.getElementById('fedit-sheet').classList.remove('open');
|
|
2881
|
+
_fedit = null;
|
|
2882
|
+
};
|
|
2883
|
+
|
|
2884
|
+
window.feditReload = function() {
|
|
2885
|
+
if (!_fedit) return;
|
|
2886
|
+
if (_fedit.dirty && !confirm('有未保存的修改,重新加载会丢弃,确定?')) return;
|
|
2887
|
+
openFileEditor(_fedit.path);
|
|
2888
|
+
};
|
|
2889
|
+
|
|
2890
|
+
async function feditSaveReq(force) {
|
|
2891
|
+
return fetch(`/api/files/${SESSION_ID}/content`, {
|
|
2892
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
2893
|
+
body: JSON.stringify({ path: _fedit.path, content: document.getElementById('fedit-ta').value, baseMtime: _fedit.mtime, force: force || undefined }),
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
window.feditSave = async function(tellAi) {
|
|
2898
|
+
if (!_fedit) return;
|
|
2899
|
+
const btn = document.getElementById(tellAi ? 'fedit-save-tell' : 'fedit-save');
|
|
2900
|
+
const orig = btn.textContent;
|
|
2901
|
+
btn.disabled = true;
|
|
2902
|
+
try {
|
|
2903
|
+
let r = await feditSaveReq(false);
|
|
2904
|
+
let d = await r.json().catch(() => ({}));
|
|
2905
|
+
if (r.status === 409 && d.error === 'conflict') {
|
|
2906
|
+
const ok = confirm('「' + _fedit.name + '」在你编辑期间被修改过(可能是 AI 刚写入)。\n\n确定 = 用我的版本覆盖\n取消 = 不保存(可点 🔄 重读最新内容)');
|
|
2907
|
+
if (!ok) { feditSetState('已取消'); return; }
|
|
2908
|
+
r = await feditSaveReq(true);
|
|
2909
|
+
d = await r.json().catch(() => ({}));
|
|
2910
|
+
}
|
|
2911
|
+
if (r.ok && d.ok) {
|
|
2912
|
+
_fedit.mtime = typeof d.mtime === 'number' ? d.mtime : Date.now();
|
|
2913
|
+
_fedit.loaded = document.getElementById('fedit-ta').value;
|
|
2914
|
+
_fedit.dirty = false;
|
|
2915
|
+
feditSetState('已存 ' + new Date().toTimeString().slice(0, 5));
|
|
2916
|
+
addStatusMessage('✏️ 已保存: ' + _fedit.path);
|
|
2917
|
+
if (tellAi) feditTellAi();
|
|
2918
|
+
} else {
|
|
2919
|
+
alert('保存失败: ' + (d.error || ('HTTP ' + r.status)));
|
|
2920
|
+
}
|
|
2921
|
+
} catch (e) {
|
|
2922
|
+
alert('保存出错: ' + e.message);
|
|
2923
|
+
} finally {
|
|
2924
|
+
btn.disabled = false;
|
|
2925
|
+
btn.textContent = orig;
|
|
2926
|
+
}
|
|
2927
|
+
};
|
|
2928
|
+
|
|
2929
|
+
// 保存后告诉 AI 重读:空闲直接发;正忙则转草稿队列,本轮干完自动接上(或手动取回)
|
|
2930
|
+
function feditTellAi() {
|
|
2931
|
+
const p = _fedit ? _fedit.path : '';
|
|
2932
|
+
closeFileEditor();
|
|
2933
|
+
if (currentSession?.mode !== 'agent') { addStatusMessage('已保存。该会话不是 agent 模式,不会通知 AI'); return; }
|
|
2934
|
+
const prompt = '我刚刚在网页编辑器里手动修改了文件 ' + p + ',请用 Read 重新读取它的最新内容,并基于修改后的版本继续后续工作。';
|
|
2935
|
+
if (workState !== 'idle') {
|
|
2936
|
+
(async () => {
|
|
2937
|
+
try {
|
|
2938
|
+
const r = await fetch('/api/inbox', {
|
|
2939
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2940
|
+
body: JSON.stringify({ text: prompt, sessionId: SESSION_ID, source: 'busy' }),
|
|
2941
|
+
});
|
|
2942
|
+
const d = await r.json();
|
|
2943
|
+
if (d.error) { addStatusMessage('存草稿失败: ' + d.error); return; }
|
|
2944
|
+
_draftTipDismissed = false;
|
|
2945
|
+
await refreshInbox();
|
|
2946
|
+
addStatusMessage('📥 AI 正在跑,已把「重读 ' + p + '」存进草稿,干完自动接上');
|
|
2947
|
+
} catch (e) { addStatusMessage('存草稿失败: ' + e.message); }
|
|
2948
|
+
})();
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
if (!isController) {
|
|
2952
|
+
if (canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
2953
|
+
else { addStatusMessage('其他用户控制中,无法发送'); return; }
|
|
2954
|
+
}
|
|
2955
|
+
endStream();
|
|
2956
|
+
addInputMessage('✏️ 我修改了 ' + p + ',请按最新内容继续');
|
|
2957
|
+
socket.emit('agent:query', { prompt });
|
|
2958
|
+
showThinking();
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2665
2961
|
// ── 文件同步(File System Access API)──────────
|
|
2666
2962
|
let syncDirHandle = null;
|
|
2667
2963
|
let syncMode = false;
|