@wendongfly/myhi 1.3.194 → 1.3.195
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/admin.html +51 -0
- package/dist/chat.html +581 -260
- package/dist/index.html +2 -2
- package/dist/index.js +10 -10
- package/dist/index.min.js +172 -165
- package/package.json +1 -1
package/dist/chat.html
CHANGED
|
@@ -454,6 +454,66 @@
|
|
|
454
454
|
.dr-btn.take { background: var(--green); color: #fff; font-weight: 600; }
|
|
455
455
|
.dr-btn:active { opacity: 0.75; }
|
|
456
456
|
.dr-src { color: var(--blue2); }
|
|
457
|
+
/* 技能面板 */
|
|
458
|
+
.skl-box { max-width: 640px !important; max-height: min(88vh, 820px); min-height: min(460px, 80vh); display: flex; flex-direction: column; padding-top: 0.6rem !important; }
|
|
459
|
+
@media (min-width: 769px) { #skill-sheet.open { align-items: center; } #skill-sheet .skl-box { border-radius: 14px; } }
|
|
460
|
+
.skl-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.1rem 0.1rem 0.55rem; border-bottom: 1px solid var(--surface2); }
|
|
461
|
+
.skl-head b { font-size: 0.92rem; }
|
|
462
|
+
.skl-back { background: none; border: none; color: var(--text2); font-size: 1.3rem; line-height: 1; cursor: pointer; padding: 0 0.3rem; border-radius: 6px; }
|
|
463
|
+
.skl-back:hover { background: var(--surface2); }
|
|
464
|
+
.skl-view { flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: thin; padding: 0.55rem 0.1rem 0.2rem; }
|
|
465
|
+
.skl-tools { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 0.5rem; }
|
|
466
|
+
.skl-tool { border: 1px solid var(--border); background: var(--surface2); color: var(--text2); border-radius: 8px; padding: 0.3rem 0.6rem; font-size: 0.74rem; cursor: pointer; white-space: nowrap; position: relative; }
|
|
467
|
+
.skl-tool.pri { background: var(--purple); border-color: var(--purple); color: #fff; font-weight: 600; }
|
|
468
|
+
.skl-badge { position: absolute; top: -6px; right: -6px; background: var(--green); color: #fff; border-radius: 8px; font-size: 0.6rem; padding: 0 0.3rem; font-weight: 700; }
|
|
469
|
+
.skl-banner { background: rgba(46,160,67,0.1); color: var(--green); border-radius: 8px; padding: 0.4rem 0.6rem; font-size: 0.74rem; cursor: pointer; margin-bottom: 0.4rem; }
|
|
470
|
+
.skl-grp { font-size: 0.68rem; color: var(--muted); font-weight: 600; padding: 0.5rem 0.2rem 0.2rem; }
|
|
471
|
+
.skl-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.3rem; border-bottom: 1px solid var(--surface2); border-radius: 6px; }
|
|
472
|
+
.skl-item:hover { background: var(--surface2); }
|
|
473
|
+
.skl-main { flex: 1; min-width: 0; cursor: pointer; }
|
|
474
|
+
.skl-name { color: var(--purple2); font-weight: 600; font-size: 0.84rem; }
|
|
475
|
+
.skl-desc { font-size: 0.72rem; color: var(--muted); line-height: 1.45; margin-top: 0.1rem; word-break: break-word; }
|
|
476
|
+
.skl-tag { display: inline-block; margin-left: 0.35rem; font-size: 0.6rem; padding: 0.02rem 0.35rem; border-radius: 8px; border: 1px solid var(--border); color: var(--muted); vertical-align: 1px; }
|
|
477
|
+
.skl-tag.skill { border-color: var(--green); color: var(--green); }
|
|
478
|
+
.skl-tag.command { border-color: var(--purple2); color: var(--purple2); }
|
|
479
|
+
.skl-acts { display: flex; gap: 0.25rem; flex-shrink: 0; }
|
|
480
|
+
.skl-empty { text-align: center; color: var(--muted); padding: 1.6rem 0.5rem; font-size: 0.8rem; line-height: 1.7; }
|
|
481
|
+
.skl-foot { font-size: 0.68rem; color: var(--muted); line-height: 1.55; padding: 0.6rem 0.2rem 0; }
|
|
482
|
+
.skl-row { display: flex; gap: 0.4rem; align-items: center; margin-bottom: 0.35rem; flex-wrap: wrap; }
|
|
483
|
+
.skl-sel { flex: 1; min-width: 9rem; background: var(--bg); color: var(--text2); border: 1px solid var(--border); border-radius: 8px; padding: 0.45rem; font-size: 0.8rem; }
|
|
484
|
+
.skl-hint { font-size: 0.72rem; color: var(--muted); line-height: 1.6; }
|
|
485
|
+
.skl-hint code { background: var(--surface2); padding: 0 0.25rem; border-radius: 3px; }
|
|
486
|
+
.skl-lbl { font-size: 0.72rem; color: var(--text2); font-weight: 600; margin: 0.6rem 0 0.3rem; }
|
|
487
|
+
.skl-radios { display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
|
488
|
+
.skl-radios label { flex: 1; min-width: 10rem; display: flex; gap: 0.4rem; align-items: flex-start; border: 1px solid var(--border); border-radius: 8px; padding: 0.45rem 0.55rem; cursor: pointer; font-size: 0.78rem; }
|
|
489
|
+
.skl-radios label b { white-space: nowrap; }
|
|
490
|
+
.skl-radios label small { display: block; color: var(--muted); font-size: 0.68rem; margin-top: 0.1rem; }
|
|
491
|
+
.skl-radios input { margin-top: 0.2rem; }
|
|
492
|
+
.skl-card { border: 1px solid var(--border); border-radius: 9px; padding: 0.5rem 0.6rem; margin-bottom: 0.45rem; font-size: 0.8rem; }
|
|
493
|
+
.skl-share-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.45rem; }
|
|
494
|
+
@media (max-width: 480px) { .skl-share-grid { grid-template-columns: 1fr; } }
|
|
495
|
+
.skl-big { text-align: left; border: 1px solid var(--border); background: var(--bg); color: var(--text); border-radius: 10px; padding: 0.55rem 0.65rem; cursor: pointer; display: grid; grid-template-columns: 1.6rem 1fr; column-gap: 0.3rem; align-items: center; }
|
|
496
|
+
.skl-big:hover { border-color: var(--purple); }
|
|
497
|
+
.skl-big span { grid-row: span 2; font-size: 1.1rem; }
|
|
498
|
+
.skl-big b { font-size: 0.8rem; }
|
|
499
|
+
.skl-big small { color: var(--muted); font-size: 0.66rem; line-height: 1.4; }
|
|
500
|
+
.skl-out { margin-top: 0.5rem; font-size: 0.76rem; color: var(--text2); line-height: 1.6; background: var(--surface2); border-radius: 8px; padding: 0.5rem 0.6rem; }
|
|
501
|
+
.skl-warn { background: rgba(248,81,73,0.1); color: var(--red); border-radius: 8px; padding: 0.45rem 0.6rem; font-size: 0.74rem; line-height: 1.55; margin: 0.4rem 0; }
|
|
502
|
+
.skl-ok { color: var(--green); font-size: 0.74rem; margin: 0.35rem 0; }
|
|
503
|
+
.skl-findings { margin: 0.35rem 0; display: flex; flex-direction: column; gap: 0.25rem; }
|
|
504
|
+
.skl-f { font-size: 0.72rem; border-radius: 6px; padding: 0.3rem 0.5rem; line-height: 1.5; }
|
|
505
|
+
.skl-f.danger { background: rgba(248,81,73,0.1); color: var(--red); }
|
|
506
|
+
.skl-f.warn { background: rgba(210,153,34,0.1); color: var(--yellow); }
|
|
507
|
+
.skl-f small { color: var(--muted); }
|
|
508
|
+
.skl-f code { display: block; margin-top: 0.15rem; font-size: 0.68rem; color: var(--text2); white-space: pre-wrap; word-break: break-all; }
|
|
509
|
+
.skl-files { margin: 0.4rem 0; font-size: 0.74rem; }
|
|
510
|
+
.skl-files summary { cursor: pointer; color: var(--blue2); }
|
|
511
|
+
.skl-files pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 0.45rem; font-size: 0.7rem; max-height: 14rem; overflow: auto; white-space: pre-wrap; word-break: break-word; }
|
|
512
|
+
.skl-fname { font-weight: 600; margin-top: 0.4rem; color: var(--text2); }
|
|
513
|
+
.skl-confirm { display: flex; gap: 0.4rem; align-items: center; font-size: 0.74rem; color: var(--text2); margin-top: 0.4rem; cursor: pointer; }
|
|
514
|
+
.skl-pick { display: flex; gap: 0.5rem; align-items: flex-start; padding: 0.45rem 0.2rem; border-bottom: 1px solid var(--surface2); cursor: pointer; }
|
|
515
|
+
.skl-pick input { margin-top: 0.2rem; }
|
|
516
|
+
.sk.skl-dot::after { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--green); margin-left: 0.25rem; vertical-align: 2px; }
|
|
457
517
|
/* 草稿面板(队列) */
|
|
458
518
|
.ib-box { max-width: 640px !important; max-height: min(86vh, 760px); min-height: min(420px, 80vh); display: flex; flex-direction: column; padding-top: 0.6rem !important; }
|
|
459
519
|
@media (min-width: 769px) { #inbox-sheet.open { align-items: center; } #inbox-sheet .ib-box { border-radius: 14px; } }
|
|
@@ -467,8 +527,15 @@
|
|
|
467
527
|
.ib-pane { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
468
528
|
.ib-bar { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.1rem 0.35rem; flex-wrap: wrap; }
|
|
469
529
|
.ib-hint { flex: 1; min-width: 8em; font-size: 0.68rem; color: var(--muted); line-height: 1.45; }
|
|
470
|
-
.ib-autoall { display: flex; align-items: center; gap: 0.
|
|
471
|
-
|
|
530
|
+
.ib-autoall { display: flex; align-items: center; gap: 0.4rem; position: relative; font-size: 0.72rem; color: var(--text2); cursor: pointer; white-space: nowrap; }
|
|
531
|
+
/* 开关样式:比「复选框 + ▶」直观,一眼看出开 / 关 */
|
|
532
|
+
.ib-autoall input { position: absolute; opacity: 0; width: 0; height: 0; }
|
|
533
|
+
.ib-sw { position: relative; width: 1.9rem; height: 1.05rem; border-radius: 1rem; background: var(--border2, var(--border)); transition: background 0.15s; flex-shrink: 0; }
|
|
534
|
+
.ib-sw::after { content: ''; position: absolute; top: 0.125rem; left: 0.125rem; width: 0.8rem; height: 0.8rem; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.25); transition: transform 0.15s; }
|
|
535
|
+
.ib-autoall input:checked + .ib-sw { background: var(--green); }
|
|
536
|
+
.ib-autoall input:checked + .ib-sw::after { transform: translateX(0.85rem); }
|
|
537
|
+
.ib-autoall input:indeterminate + .ib-sw { background: rgba(46,160,67,0.45); }
|
|
538
|
+
.ib-autoall input:focus-visible + .ib-sw { outline: 2px solid var(--green); outline-offset: 2px; }
|
|
472
539
|
.ib-paused { display: flex; align-items: center; gap: 0.5rem; font-size: 0.7rem; color: var(--yellow); background: rgba(210,153,34,0.10); border-radius: 8px; padding: 0.35rem 0.55rem; margin-bottom: 0.3rem; }
|
|
473
540
|
.ib-paused span { flex: 1; }
|
|
474
541
|
.ib-list { flex: 1; min-height: 3rem; overflow-y: auto; scrollbar-width: thin; }
|
|
@@ -989,8 +1056,8 @@
|
|
|
989
1056
|
<div class="ib-bar">
|
|
990
1057
|
<span id="ib-hint" class="ib-hint"></span>
|
|
991
1058
|
<button id="ib-sel-btn" class="dr-btn" onclick="ibToggleSelect()" title="勾选几条后合并、一起取回或删除">☑ 多选</button>
|
|
992
|
-
<label class="ib-autoall" title="
|
|
993
|
-
<input type="checkbox" id="ib-auto-all" onchange="ibAutoAll(this.checked)"
|
|
1059
|
+
<label id="ib-autoall" class="ib-autoall" title="打开后:每干完一轮,自动按顺序发出下一条(每条都是一轮真实的模型调用)。上一轮被中断或出错时会暂停,等你确认再继续。">
|
|
1060
|
+
<input type="checkbox" id="ib-auto-all" onchange="ibAutoAll(this.checked)"><span class="ib-sw"></span>依次自动发送
|
|
994
1061
|
</label>
|
|
995
1062
|
</div>
|
|
996
1063
|
<div id="ib-paused" class="ib-paused" style="display:none">
|
|
@@ -1199,59 +1266,113 @@
|
|
|
1199
1266
|
|
|
1200
1267
|
<div id="skill-sheet" class="action-sheet">
|
|
1201
1268
|
<div class="action-sheet-backdrop" onclick="closeSkillSheet()"></div>
|
|
1202
|
-
<div class="action-sheet-box
|
|
1203
|
-
<div class="
|
|
1204
|
-
<
|
|
1205
|
-
<
|
|
1206
|
-
|
|
1207
|
-
<button onclick="showSkillImport()" style="background:var(--surface2);color:var(--text2);border:1px solid var(--border);border-radius:6px;padding:0.3rem 0.7rem;font-size:0.75rem;cursor:pointer">Git</button>
|
|
1208
|
-
<button onclick="showSkillEditor()" style="background:var(--purple);color:#fff;border:none;border-radius:6px;padding:0.3rem 0.7rem;font-size:0.75rem;cursor:pointer">+ 新建</button>
|
|
1209
|
-
</div>
|
|
1269
|
+
<div class="action-sheet-box skl-box">
|
|
1270
|
+
<div class="skl-head">
|
|
1271
|
+
<button id="skl-back" class="skl-back" onclick="sklView('list')" style="display:none" title="返回">‹</button>
|
|
1272
|
+
<b id="skill-sheet-title">技能</b>
|
|
1273
|
+
<button class="ib-x" onclick="closeSkillSheet()" title="关闭">✕</button>
|
|
1210
1274
|
</div>
|
|
1211
|
-
<!--
|
|
1212
|
-
<div id="
|
|
1213
|
-
<div
|
|
1275
|
+
<!-- 列表 -->
|
|
1276
|
+
<div id="skl-v-list" class="skl-view">
|
|
1277
|
+
<div class="skl-tools">
|
|
1278
|
+
<button class="skl-tool pri" onclick="sklView('gen')" title="把本次任务里的做法提炼成技能,以后一点就能复用">✨ 从本次任务生成</button>
|
|
1279
|
+
<button class="skl-tool" onclick="sklView('import')" title="粘贴分享码 / 分享链接,或选择技能文件">📥 导入</button>
|
|
1280
|
+
<button class="skl-tool" onclick="sklView('team')" title="团队技能仓库:订阅、检查更新">👥 团队仓库<span id="skl-upd-badge" class="skl-badge" style="display:none"></span></button>
|
|
1281
|
+
<button class="skl-tool" onclick="showSkillMarket()">🛒 市场</button>
|
|
1282
|
+
<button class="skl-tool" onclick="showSkillEditor()">+ 手写</button>
|
|
1283
|
+
</div>
|
|
1284
|
+
<div id="skl-upd-banner" class="skl-banner" style="display:none" onclick="sklView('team')"></div>
|
|
1285
|
+
<div id="skill-list-view" class="skl-list"></div>
|
|
1286
|
+
<div class="skl-foot">技能 = 预先写好的一段做事方法。<b>手动技能</b>点快捷栏按钮(或输入 /名称)运行;<b>自动技能</b>不用点,AI 遇到合适的场景会自己用。</div>
|
|
1214
1287
|
</div>
|
|
1215
|
-
<!--
|
|
1216
|
-
<div id="
|
|
1217
|
-
<div
|
|
1218
|
-
<input id="skill-name" class="slash-inp" placeholder="
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1288
|
+
<!-- 编辑 -->
|
|
1289
|
+
<div id="skl-v-edit" class="skl-view" style="display:none">
|
|
1290
|
+
<div class="skl-row">
|
|
1291
|
+
<input id="skill-name" class="slash-inp" placeholder="名称(英文,如 deploy-test)" autocomplete="off" style="flex:1">
|
|
1292
|
+
</div>
|
|
1293
|
+
<div class="skl-row">
|
|
1294
|
+
<select id="skill-kind" class="slash-inp skl-sel" onchange="sklKindChanged()">
|
|
1295
|
+
<option value="command">手动技能(/命令,点按钮运行)</option>
|
|
1296
|
+
<option value="skill">自动技能(AI 遇到合适场景自己用)</option>
|
|
1297
|
+
</select>
|
|
1298
|
+
<select id="skill-scope" class="slash-inp skl-sel">
|
|
1299
|
+
<option value="project">只在本项目</option>
|
|
1300
|
+
<option value="user">本机所有项目</option>
|
|
1222
1301
|
</select>
|
|
1223
|
-
<textarea id="skill-content" class="slash-inp" placeholder="# 技能标题 技能描述和指令内容..." rows="10" style="resize:vertical;font-family:monospace;font-size:0.78rem;line-height:1.5"></textarea>
|
|
1224
1302
|
</div>
|
|
1225
|
-
<
|
|
1226
|
-
<
|
|
1303
|
+
<textarea id="skill-content" class="slash-inp" rows="14" style="resize:vertical;font-family:monospace;font-size:0.78rem;line-height:1.5;width:100%;box-sizing:border-box"></textarea>
|
|
1304
|
+
<div id="skill-edit-hint" class="skl-hint"></div>
|
|
1305
|
+
<button class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-top:0.5rem" onclick="saveSkill()">保存</button>
|
|
1227
1306
|
</div>
|
|
1228
|
-
<!--
|
|
1229
|
-
<div id="
|
|
1230
|
-
<div style="
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
<
|
|
1236
|
-
|
|
1237
|
-
|
|
1307
|
+
<!-- 从本次任务生成 -->
|
|
1308
|
+
<div id="skl-v-gen" class="skl-view" style="display:none">
|
|
1309
|
+
<div class="skl-hint" style="margin-bottom:0.5rem">让正在干活的 AI 把<b>本次任务里的做法</b>提炼成可复用的技能:项目特有的路径、名字会改成参数,不会写进密码等敏感信息。AI <b>先出草稿给你确认</b>,确认后才保存。</div>
|
|
1310
|
+
<textarea id="skl-gen-hint" class="slash-inp" rows="3" placeholder="想沉淀成什么技能?例如「把今天这套部署到测试环境的流程做成技能」。可以留空,让 AI 自己判断" style="width:100%;box-sizing:border-box;resize:vertical"></textarea>
|
|
1311
|
+
<div class="skl-lbl">技能类型</div>
|
|
1312
|
+
<div class="skl-radios">
|
|
1313
|
+
<label><input type="radio" name="skl-gen-kind" value="command" checked> <b>手动技能</b><small>点按钮 / 输入 /名称 才运行,最直观</small></label>
|
|
1314
|
+
<label><input type="radio" name="skl-gen-kind" value="skill"> <b>自动技能</b><small>AI 遇到合适场景自己用,可带辅助脚本</small></label>
|
|
1315
|
+
</div>
|
|
1316
|
+
<div class="skl-lbl">保存到</div>
|
|
1317
|
+
<div class="skl-radios">
|
|
1318
|
+
<label><input type="radio" name="skl-gen-scope" value="project" checked> <b>只在本项目</b><small>跟着项目目录走</small></label>
|
|
1319
|
+
<label><input type="radio" name="skl-gen-scope" value="user"> <b>本机所有项目</b><small>这台电脑上的任务都能用</small></label>
|
|
1320
|
+
</div>
|
|
1321
|
+
<button class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-top:0.6rem" onclick="genSkillFromTask()">✨ 让 AI 起草</button>
|
|
1322
|
+
</div>
|
|
1323
|
+
<!-- 分享 -->
|
|
1324
|
+
<div id="skl-v-share" class="skl-view" style="display:none">
|
|
1325
|
+
<div id="skl-share-info" class="skl-card"></div>
|
|
1326
|
+
<div id="skl-share-warn" class="skl-warn" style="display:none"></div>
|
|
1327
|
+
<div class="skl-share-grid">
|
|
1328
|
+
<button class="skl-big" onclick="sklShareDownload()"><span>⬇</span><b>下载技能文件</b><small>微信 / 邮件发给对方,对方「📥 导入」</small></button>
|
|
1329
|
+
<button class="skl-big" onclick="sklShareCode()"><span>📋</span><b>复制分享码</b><small>一段文本,粘贴给对方导入</small></button>
|
|
1330
|
+
<button class="skl-big" onclick="sklShareLink()"><span>🔗</span><b>生成分享链接</b><small>同一局域网 / Tailscale 的 myhi 直接导入,24 小时有效</small></button>
|
|
1331
|
+
<button class="skl-big" onclick="sklSharePublishForm()"><span>👥</span><b>发布到团队仓库</b><small>推到 Git 仓库,订阅的人会收到更新</small></button>
|
|
1332
|
+
</div>
|
|
1333
|
+
<div id="skl-share-out" class="skl-out" style="display:none"></div>
|
|
1334
|
+
<div id="skl-pub-form" style="display:none;margin-top:0.5rem">
|
|
1335
|
+
<input id="skl-pub-url" class="slash-inp" placeholder="仓库地址(https://… 或 git@…)" autocomplete="off" style="width:100%;box-sizing:border-box;margin-bottom:0.35rem">
|
|
1336
|
+
<div class="skl-row">
|
|
1337
|
+
<input id="skl-pub-user" class="slash-inp" placeholder="用户名(可选)" autocomplete="off" style="flex:1">
|
|
1338
|
+
<input id="skl-pub-pass" class="slash-inp" type="password" placeholder="Token(需要写权限)" autocomplete="off" style="flex:1">
|
|
1339
|
+
<input id="skl-pub-branch" class="slash-inp" placeholder="分支(默认)" autocomplete="off" style="width:6.5rem">
|
|
1238
1340
|
</div>
|
|
1239
|
-
<
|
|
1240
|
-
|
|
1241
|
-
|
|
1341
|
+
<button class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-top:0.4rem" onclick="sklPublish()">发布</button>
|
|
1342
|
+
</div>
|
|
1343
|
+
</div>
|
|
1344
|
+
<!-- 导入 -->
|
|
1345
|
+
<div id="skl-v-import" class="skl-view" style="display:none">
|
|
1346
|
+
<textarea id="skl-imp-text" class="slash-inp" rows="3" placeholder="粘贴分享码(MYHI-SKILL:…)或分享链接(…/s/skill/…)" style="width:100%;box-sizing:border-box;resize:vertical"></textarea>
|
|
1347
|
+
<div class="skl-row" style="margin-top:0.35rem">
|
|
1348
|
+
<button class="dr-btn" onclick="document.getElementById('skl-imp-file').click()">📄 选择技能文件</button>
|
|
1349
|
+
<input type="file" id="skl-imp-file" accept=".myhiskill,.json,.md" style="display:none" onchange="sklImportFile(this.files[0]); this.value='';">
|
|
1350
|
+
<button class="dr-btn take" style="margin-left:auto" onclick="sklImportPreview()">预览</button>
|
|
1351
|
+
</div>
|
|
1352
|
+
<div id="skl-imp-preview"></div>
|
|
1353
|
+
</div>
|
|
1354
|
+
<!-- 团队仓库 -->
|
|
1355
|
+
<div id="skl-v-team" class="skl-view" style="display:none">
|
|
1356
|
+
<div id="skl-subs"></div>
|
|
1357
|
+
<div class="skl-lbl" style="margin-top:0.6rem">添加团队仓库</div>
|
|
1358
|
+
<input id="skill-git-url" class="slash-inp" placeholder="Git 仓库地址(https://… 或 git@…)" autocomplete="off" style="width:100%;box-sizing:border-box;margin-bottom:0.35rem">
|
|
1359
|
+
<div class="skl-row">
|
|
1360
|
+
<input id="skill-git-user" class="slash-inp" placeholder="用户名(可选)" autocomplete="off" style="flex:1">
|
|
1361
|
+
<input id="skill-git-pass" class="slash-inp" type="password" placeholder="密码 / Token(可选)" autocomplete="off" style="flex:1">
|
|
1362
|
+
</div>
|
|
1363
|
+
<div class="skl-row">
|
|
1364
|
+
<select id="skill-import-scope" class="slash-inp skl-sel">
|
|
1365
|
+
<option value="user">装到本机所有项目</option>
|
|
1366
|
+
<option value="project">只装到本项目</option>
|
|
1242
1367
|
</select>
|
|
1368
|
+
<button class="dr-btn take" onclick="scanGitSkills()">扫描</button>
|
|
1243
1369
|
</div>
|
|
1244
|
-
<div id="skill-import-preview"
|
|
1245
|
-
<div id="skill-import-scan-btn"></div>
|
|
1246
|
-
<button id="skill-import-install-btn" class="action-sheet-cancel" style="display:none;background:var(--green);color:#000;font-weight:600;margin-bottom:0.4rem" onclick="installGitSkills()">安装选中的技能</button>
|
|
1247
|
-
<button class="action-sheet-cancel" onclick="backToSkillList()">返回列表</button>
|
|
1370
|
+
<div id="skill-import-preview"></div>
|
|
1248
1371
|
</div>
|
|
1249
|
-
<!--
|
|
1250
|
-
<div id="
|
|
1372
|
+
<!-- 市场 -->
|
|
1373
|
+
<div id="skl-v-market" class="skl-view" style="display:none">
|
|
1251
1374
|
<div id="skill-market-list"></div>
|
|
1252
|
-
<button class="action-sheet-cancel" onclick="backToSkillList()" style="margin-top:0.5rem">返回列表</button>
|
|
1253
1375
|
</div>
|
|
1254
|
-
<button id="skill-close-btn" class="action-sheet-cancel" onclick="closeSkillSheet()">关闭</button>
|
|
1255
1376
|
</div>
|
|
1256
1377
|
</div>
|
|
1257
1378
|
|
|
@@ -2689,6 +2810,7 @@
|
|
|
2689
2810
|
try { updateInjectBtn(); } catch {}
|
|
2690
2811
|
// 用 try 包住:这两个绑定声明在文件后段,初始化早期调用会撞 TDZ
|
|
2691
2812
|
try { if (!busy) _draftTipDismissed = false; renderDraftTip(); } catch {}
|
|
2813
|
+
try { if (!busy && _sklGenPending) { const sc = _sklGenPending; _sklGenPending = false; loadSkills(); fetch('/api/skills/changed', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: SESSION_ID, scope: sc }) }).catch(() => {}); } } catch {}
|
|
2692
2814
|
try { if (!busy) _ibIdleSince = Date.now(); if (document.getElementById('inbox-sheet').classList.contains('open')) renderInbox(); } catch {}
|
|
2693
2815
|
try { if (!busy) checkGuide(); else renderGuideTip(); } catch {}
|
|
2694
2816
|
}
|
|
@@ -3821,7 +3943,7 @@
|
|
|
3821
3943
|
followup: ['🔄 跟进', '长任务交出去后,AI 每说完一轮,隔一段时间自动替你问进度、让它接着干;判断全部完成就停(最多 5 次,每次都算一轮模型调用)。'],
|
|
3822
3944
|
git: ['提交', '查看改动并 git 提交 / 推送。'],
|
|
3823
3945
|
memory: ['记忆', '查看、编辑「项目说明」和 AI 的长期记忆,每次新对话都会带上;长期项目可一键让 AI 生成项目说明。'],
|
|
3824
|
-
skills: ['技能', '
|
|
3946
|
+
skills: ['技能', '把做事方法存成技能,以后一点就能复用:可从本次任务一键生成,分享给其他 myhi(文件 / 分享码 / 链接 / 团队仓库),导入前自动做安全检查。'],
|
|
3825
3947
|
cost: ['费用', '查看本次会话的 token 用量与花费。'],
|
|
3826
3948
|
};
|
|
3827
3949
|
document.querySelectorAll('.sk[data-sk]').forEach(b => { const h = SK_HELP[b.dataset.sk]; if (h && !b.title) b.title = h[1]; });
|
|
@@ -5303,6 +5425,8 @@
|
|
|
5303
5425
|
const selBtn = document.getElementById('ib-sel-btn');
|
|
5304
5426
|
selBtn.textContent = _ibSelMode ? '完成' : '☑ 多选';
|
|
5305
5427
|
selBtn.style.display = mine.length > 1 || _ibSelMode ? '' : 'none';
|
|
5428
|
+
// 多选时只管勾选和批量操作,自动发送开关先藏起来;没草稿时也不显示
|
|
5429
|
+
document.getElementById('ib-autoall').style.display = _ibSelMode || !mine.length ? 'none' : '';
|
|
5306
5430
|
|
|
5307
5431
|
// 暂停提示:有待自动发送的草稿,会话却已空闲一阵(上一轮被中断 / 出错时不会自动往下发)
|
|
5308
5432
|
clearTimeout(_ibPauseTimer);
|
|
@@ -5922,6 +6046,7 @@
|
|
|
5922
6046
|
const customs = _customSkills.filter(s => s.source !== 'builtin');
|
|
5923
6047
|
// 内置技能:始终逐个显示(青绿色)
|
|
5924
6048
|
const builtinEl = document.getElementById('sk-builtin-skills');
|
|
6049
|
+
loadSubs(); // 团队仓库有更新时快捷栏「技能」按钮带个绿点
|
|
5925
6050
|
builtinEl.innerHTML = builtins.map(s =>
|
|
5926
6051
|
`<button class="sk sk-claude" onclick="runSkill('${escHtml(s.name)}')" title="${escHtml(s.desc)}" style="color:var(--teal)">/${escHtml(s.name)}</button>`
|
|
5927
6052
|
).join('');
|
|
@@ -5930,14 +6055,15 @@
|
|
|
5930
6055
|
if (!customs.length) { customEl.innerHTML = ''; return; }
|
|
5931
6056
|
if (customs.length <= 3) {
|
|
5932
6057
|
customEl.innerHTML = customs.map(s =>
|
|
5933
|
-
`<button class="sk sk-claude" onclick="runSkill('${escHtml(s.name)}')" title="${escHtml(s.desc)}" style="color:var(--purple2)"
|
|
6058
|
+
`<button class="sk sk-claude" onclick="runSkill('${escHtml(s.name)}', false, '${s.kind || 'command'}')" title="${escHtml(s.desc)}${s.kind === 'skill' ? '(自动技能:AI 也会在合适时自己用)' : ''}" style="color:var(--purple2)">${s.kind === 'skill' ? '🧠' : '/'}${escHtml(s.name)}</button>`
|
|
5934
6059
|
).join('');
|
|
5935
6060
|
} else {
|
|
5936
6061
|
customEl.innerHTML = `<button class="sk sk-claude" onclick="openCmdSheet()" style="color:var(--purple2)">/命令 (${customs.length})</button>`;
|
|
5937
6062
|
}
|
|
5938
6063
|
} catch {}
|
|
5939
6064
|
}
|
|
5940
|
-
window.runSkill = function(name, skipPrompt) {
|
|
6065
|
+
window.runSkill = function(name, skipPrompt, kind) {
|
|
6066
|
+
_pendingSkillKind = kind || _customSkills.find(s => s.name === name)?.kind || 'command';
|
|
5941
6067
|
// 直接运行(从弹窗确认后调用 skipPrompt=true)
|
|
5942
6068
|
if (skipPrompt) {
|
|
5943
6069
|
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
@@ -5948,10 +6074,10 @@
|
|
|
5948
6074
|
return;
|
|
5949
6075
|
}
|
|
5950
6076
|
// 先弹窗让用户补充要求
|
|
5951
|
-
const skill = _customSkills.find(s => s.name === name);
|
|
6077
|
+
const skill = _customSkills.find(s => s.name === name && (s.kind || 'command') === _pendingSkillKind);
|
|
5952
6078
|
openSkillRunSheet(name, skill?.desc);
|
|
5953
6079
|
};
|
|
5954
|
-
let _pendingSkillName = null;
|
|
6080
|
+
let _pendingSkillName = null, _pendingSkillKind = 'command';
|
|
5955
6081
|
window.openSkillRunSheet = function(name, desc) {
|
|
5956
6082
|
_pendingSkillName = name;
|
|
5957
6083
|
const sheet = document.getElementById('skill-run-sheet');
|
|
@@ -5973,6 +6099,13 @@
|
|
|
5973
6099
|
closeSkillRunSheet();
|
|
5974
6100
|
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
5975
6101
|
const displayCmd = args ? `/${name} ${args}` : `/${name}`;
|
|
6102
|
+
if (_pendingSkillKind === 'skill') {
|
|
6103
|
+
// 自动技能(Agent 技能)没有 /命令:明确请 AI 用它
|
|
6104
|
+
socket.emit('agent:query', { prompt: `请使用「${name}」技能${args ? '完成:' + args : '。'}` });
|
|
6105
|
+
addInputMessage(`🧠 ${name}${args ? ' ' + args : ''}`);
|
|
6106
|
+
showThinking();
|
|
6107
|
+
return;
|
|
6108
|
+
}
|
|
5976
6109
|
if (skill?.source === 'builtin') {
|
|
5977
6110
|
// 内置技能不在 .claude/commands/ 里,需拉取内容后直接发送为 prompt
|
|
5978
6111
|
try {
|
|
@@ -6005,8 +6138,8 @@
|
|
|
6005
6138
|
const filtered = _customSkills.filter(s =>
|
|
6006
6139
|
s.source !== 'builtin' && (!kw || s.name.toLowerCase().includes(kw) || (s.desc || '').toLowerCase().includes(kw)));
|
|
6007
6140
|
listEl.innerHTML = filtered.length
|
|
6008
|
-
? filtered.map(s => `<div class="action-sheet-item" onclick="runSkillAndClose('${escHtml(s.name)}')">
|
|
6009
|
-
<div><div style="color:var(--purple3)"
|
|
6141
|
+
? filtered.map(s => `<div class="action-sheet-item" onclick="runSkillAndClose('${escHtml(s.name)}', '${s.kind || 'command'}')">
|
|
6142
|
+
<div><div style="color:var(--purple3)">${s.kind === 'skill' ? '🧠 ' : '/'}${escHtml(s.name)}</div>${s.desc ? `<div class="desc">${escHtml(s.desc)}</div>` : ''}</div>
|
|
6010
6143
|
</div>`).join('')
|
|
6011
6144
|
: '<div style="text-align:center;color:var(--muted);padding:1.5rem 0;font-size:0.82rem">无匹配命令</div>';
|
|
6012
6145
|
};
|
|
@@ -6018,9 +6151,9 @@
|
|
|
6018
6151
|
window.closeCmdSheet = function() {
|
|
6019
6152
|
document.getElementById('cmd-sheet').classList.remove('open');
|
|
6020
6153
|
};
|
|
6021
|
-
window.runSkillAndClose = function(name) {
|
|
6154
|
+
window.runSkillAndClose = function(name, kind) {
|
|
6022
6155
|
closeCmdSheet();
|
|
6023
|
-
runSkill(name);
|
|
6156
|
+
runSkill(name, false, kind);
|
|
6024
6157
|
};
|
|
6025
6158
|
// 加入会话后加载 skill
|
|
6026
6159
|
socket.on('joined', () => {
|
|
@@ -6032,252 +6165,440 @@
|
|
|
6032
6165
|
if (document.getElementById('inbox-sheet')?.classList.contains('open')) renderInbox();
|
|
6033
6166
|
});
|
|
6034
6167
|
|
|
6035
|
-
// ──
|
|
6036
|
-
|
|
6168
|
+
// ── 技能管理(见 src/skills.js)────────────────────────
|
|
6169
|
+
// 手动技能 = Claude 的斜杠命令(.claude/commands/名.md);自动技能 = Agent 技能(.claude/skills/名/SKILL.md,AI 自己调用)。
|
|
6170
|
+
// ✨ 从本次任务生成(AI 起草、确认后保存)、📤 分享(文件 / 分享码 / 链接 / 团队仓库)、📥 导入(先预览 + 安全扫描)、👥 团队仓库订阅。
|
|
6171
|
+
let _editingSkill = null; // 编辑中的 { name, source, kind },null = 新建
|
|
6172
|
+
let _sklAll = [], _sklDirs = null, _sklShare = null, _sklImp = null, _gitSkills = [], _sklSubs = [];
|
|
6173
|
+
const SKL_TITLES = { list: '技能', edit: '编辑技能', gen: '✨ 从本次任务生成技能', share: '📤 分享技能', import: '📥 导入技能', team: '👥 团队技能仓库', market: '🛒 技能市场' };
|
|
6174
|
+
const sklEsc = (s) => escHtml(String(s ?? '')).replace(/"/g, '"').replace(/'/g, ''');
|
|
6175
|
+
const sklKindLabel = (k) => k === 'skill' ? '自动' : '手动';
|
|
6176
|
+
const sklScopeLabel = (s) => s === 'project' ? '本项目' : s === 'user' ? '本机' : '内置';
|
|
6177
|
+
async function sklJson(url, opts) {
|
|
6178
|
+
const r = await fetch(url, opts);
|
|
6179
|
+
const d = await r.json().catch(() => ({}));
|
|
6180
|
+
if (!r.ok || d.ok === false || d.error) { const e = new Error(d.error || ('HTTP ' + r.status)); e.data = d; throw e; }
|
|
6181
|
+
return d;
|
|
6182
|
+
}
|
|
6183
|
+
const sklPost = (url, body) => sklJson(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
|
|
6037
6184
|
|
|
6185
|
+
window.sklView = function(v) {
|
|
6186
|
+
for (const k of Object.keys(SKL_TITLES)) { const el = document.getElementById('skl-v-' + k); if (el) el.style.display = k === v ? '' : 'none'; }
|
|
6187
|
+
document.getElementById('skill-sheet-title').textContent = SKL_TITLES[v] || '技能';
|
|
6188
|
+
document.getElementById('skl-back').style.display = v === 'list' ? 'none' : '';
|
|
6189
|
+
if (v === 'list') refreshSkillList();
|
|
6190
|
+
if (v === 'team') renderSubs();
|
|
6191
|
+
if (v === 'import') { document.getElementById('skl-imp-preview').innerHTML = ''; setTimeout(() => document.getElementById('skl-imp-text').focus(), 80); }
|
|
6192
|
+
};
|
|
6038
6193
|
window.openSkillSheet = async function() {
|
|
6039
6194
|
document.getElementById('skill-sheet').classList.add('open');
|
|
6040
|
-
|
|
6041
|
-
document.getElementById('skill-edit-view').style.display = 'none';
|
|
6042
|
-
document.getElementById('skill-close-btn').style.display = '';
|
|
6043
|
-
await refreshSkillList();
|
|
6044
|
-
};
|
|
6045
|
-
window.closeSkillSheet = function() {
|
|
6046
|
-
document.getElementById('skill-sheet').classList.remove('open');
|
|
6195
|
+
sklView('list');
|
|
6047
6196
|
};
|
|
6197
|
+
window.closeSkillSheet = function() { document.getElementById('skill-sheet').classList.remove('open'); };
|
|
6198
|
+
window.backToSkillList = function() { sklView('list'); };
|
|
6048
6199
|
|
|
6049
6200
|
async function refreshSkillList() {
|
|
6050
6201
|
const listEl = document.getElementById('skill-list-view');
|
|
6051
|
-
listEl.innerHTML = '<div
|
|
6202
|
+
listEl.innerHTML = '<div class="skl-empty">加载中…</div>';
|
|
6052
6203
|
try {
|
|
6053
|
-
const
|
|
6054
|
-
|
|
6055
|
-
const
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
return;
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6204
|
+
const data = await sklJson(`/api/skills?sessionId=${SESSION_ID}`);
|
|
6205
|
+
_sklAll = data.skills || []; _sklDirs = data.dirs || null;
|
|
6206
|
+
const own = _sklAll.filter(s => s.source !== 'builtin');
|
|
6207
|
+
const group = (src, title) => {
|
|
6208
|
+
const items = own.filter(s => s.source === src);
|
|
6209
|
+
if (!items.length) return '';
|
|
6210
|
+
return `<div class="skl-grp">${title}</div>` + items.map(s => `
|
|
6211
|
+
<div class="skl-item">
|
|
6212
|
+
<div class="skl-main" onclick="editSkill('${sklEsc(s.name)}','${s.source}','${s.kind}')" title="点击编辑">
|
|
6213
|
+
<div><span class="skl-name">${s.kind === 'skill' ? '🧠 ' : '/'}${escHtml(s.name)}</span><span class="skl-tag ${s.kind}">${sklKindLabel(s.kind)}</span>${s.fileCount > 1 ? `<span class="skl-tag">${s.fileCount} 个文件</span>` : ''}</div>
|
|
6214
|
+
<div class="skl-desc">${escHtml(s.desc || '(没有说明)')}</div>
|
|
6215
|
+
</div>
|
|
6216
|
+
<div class="skl-acts">
|
|
6217
|
+
<button class="ib-ic" onclick="openSkillShare('${sklEsc(s.name)}','${s.source}','${s.kind}')" title="分享给其他 myhi">📤</button>
|
|
6218
|
+
<button class="ib-ic" onclick="deleteSkill('${sklEsc(s.name)}','${s.source}','${s.kind}')" title="删除">🗑</button>
|
|
6219
|
+
</div>
|
|
6220
|
+
</div>`).join('');
|
|
6221
|
+
};
|
|
6222
|
+
listEl.innerHTML = own.length ? group('project', '本项目') + group('user', '本机所有项目')
|
|
6223
|
+
: '<div class="skl-empty">还没有技能。<br>做完一件以后还会再做的事,点上面「✨ 从本次任务生成」,下次一点就能复用。</div>';
|
|
6069
6224
|
} catch (e) {
|
|
6070
|
-
listEl.innerHTML = `<div style="
|
|
6071
|
-
}
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
document.getElementById('skill-
|
|
6082
|
-
document.getElementById('skill-
|
|
6083
|
-
document.getElementById('skill-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6225
|
+
listEl.innerHTML = `<div class="skl-empty" style="color:var(--red)">加载失败:${escHtml(e.message)}</div>`;
|
|
6226
|
+
}
|
|
6227
|
+
sklCheckSubsQuietly();
|
|
6228
|
+
}
|
|
6229
|
+
|
|
6230
|
+
// ── 编辑 / 手写 ──
|
|
6231
|
+
const SKL_TPL = {
|
|
6232
|
+
command: '# 技能标题(一句话说明做什么)\n\n## 目标\n\n## 步骤\n1. \n2. \n\n## 注意事项\n\n(运行时用户补充的内容会替换下面的 $ARGUMENTS)\n$ARGUMENTS\n',
|
|
6233
|
+
skill: '---\nname: {{name}}\ndescription: 什么时候该用这个技能(写清楚场景,AI 据此判断要不要用)\n---\n\n# 技能标题\n\n## 步骤\n1. \n2. \n\n## 注意事项\n',
|
|
6234
|
+
};
|
|
6235
|
+
window.sklKindChanged = function() {
|
|
6236
|
+
const kind = document.getElementById('skill-kind').value;
|
|
6237
|
+
const ta = document.getElementById('skill-content');
|
|
6238
|
+
if (!_editingSkill && (!ta.value.trim() || Object.values(SKL_TPL).some(t => ta.value === t.replace('{{name}}', document.getElementById('skill-name').value.trim() || 'my-skill')))) {
|
|
6239
|
+
ta.value = SKL_TPL[kind].replace('{{name}}', document.getElementById('skill-name').value.trim() || 'my-skill');
|
|
6240
|
+
}
|
|
6241
|
+
document.getElementById('skill-edit-hint').innerHTML = (kind === 'skill'
|
|
6242
|
+
? '自动技能:名字只能用<b>小写字母、数字和连字符</b>(如 weekly-report);开头 <code>description</code> 写清楚<b>什么时候用</b>,AI 遇到这类场景会自己调用。辅助脚本等其他文件放在同目录(可让 AI 帮你加)。'
|
|
6243
|
+
: '手动技能:名字用英文、数字、- _ .(a:b 可分组);第一行标题会显示成说明;<code>$ARGUMENTS</code> 会换成运行时你补充的内容。') + '<div id="skl-name-fix"></div>';
|
|
6244
|
+
sklNameCheck();
|
|
6245
|
+
};
|
|
6246
|
+
// 自动技能名字不合规:实时提示并给出改好的名字(一点就用)
|
|
6247
|
+
const sklSlug = (n) => (String(n || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').replace(/anthropic|claude/g, 'ai').slice(0, 64).replace(/-+$/, '') || 'my-skill');
|
|
6248
|
+
window.sklNameCheck = function() {
|
|
6249
|
+
const box = document.getElementById('skl-name-fix');
|
|
6250
|
+
if (!box) return;
|
|
6251
|
+
const n = document.getElementById('skill-name').value.trim();
|
|
6252
|
+
const kind = document.getElementById('skill-kind').value;
|
|
6253
|
+
const bad = kind === 'skill' && n && (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(n) || n.length > 64 || /anthropic|claude/.test(n));
|
|
6254
|
+
box.innerHTML = bad ? `<span style="color:var(--yellow)">⚠️ 这个名字 Claude 可能认不出来,</span><a style="color:var(--blue2);cursor:pointer" onclick="document.getElementById('skill-name').value='${sklSlug(n)}';sklNameCheck()">改成 ${sklSlug(n)}</a>` : '';
|
|
6255
|
+
};
|
|
6256
|
+
document.getElementById('skill-name')?.addEventListener('input', () => sklNameCheck());
|
|
6257
|
+
window.showSkillEditor = function(name, scope, content, kind) {
|
|
6258
|
+
_editingSkill = name ? { name, source: scope, kind: kind || 'command' } : null;
|
|
6259
|
+
sklView('edit');
|
|
6260
|
+
document.getElementById('skill-sheet-title').textContent = name ? `编辑 ${kind === 'skill' ? '🧠 ' : '/'}${name}` : '手写新技能';
|
|
6261
|
+
const n = document.getElementById('skill-name'); n.value = name || ''; n.disabled = !!name;
|
|
6262
|
+
document.getElementById('skill-kind').value = kind || 'command';
|
|
6263
|
+
document.getElementById('skill-kind').disabled = !!name;
|
|
6264
|
+
document.getElementById('skill-scope').value = scope || 'project';
|
|
6265
|
+
document.getElementById('skill-scope').disabled = !!name;
|
|
6266
|
+
document.getElementById('skill-content').value = content || '';
|
|
6267
|
+
sklKindChanged();
|
|
6268
|
+
};
|
|
6269
|
+
window.editSkill = async function(name, source, kind) {
|
|
6087
6270
|
try {
|
|
6088
|
-
const
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
showSkillEditor(name, source, skill?.content || '');
|
|
6092
|
-
} catch {}
|
|
6271
|
+
const d = await sklJson(`/api/skills/content?sessionId=${SESSION_ID}&name=${encodeURIComponent(name)}&source=${source}&kind=${kind}`);
|
|
6272
|
+
showSkillEditor(name, source, d.content || '', kind);
|
|
6273
|
+
} catch (e) { addStatusMessage('打开失败:' + e.message); }
|
|
6093
6274
|
};
|
|
6094
|
-
|
|
6095
|
-
window.backToSkillList = function() {
|
|
6096
|
-
document.getElementById('skill-list-view').style.display = '';
|
|
6097
|
-
document.getElementById('skill-edit-view').style.display = 'none';
|
|
6098
|
-
document.getElementById('skill-import-view').style.display = 'none';
|
|
6099
|
-
document.getElementById('skill-market-view').style.display = 'none';
|
|
6100
|
-
document.getElementById('skill-close-btn').style.display = '';
|
|
6101
|
-
document.getElementById('skill-sheet-title').textContent = '技能管理';
|
|
6102
|
-
refreshSkillList();
|
|
6103
|
-
};
|
|
6104
|
-
|
|
6105
6275
|
window.saveSkill = async function() {
|
|
6106
6276
|
const name = document.getElementById('skill-name').value.trim();
|
|
6107
6277
|
const scope = document.getElementById('skill-scope').value;
|
|
6278
|
+
const kind = document.getElementById('skill-kind').value;
|
|
6108
6279
|
const content = document.getElementById('skill-content').value;
|
|
6109
6280
|
if (!name) { addStatusMessage('请输入技能名称'); return; }
|
|
6110
6281
|
if (!content.trim()) { addStatusMessage('请输入技能内容'); return; }
|
|
6111
6282
|
try {
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6283
|
+
await sklPost(`/api/skills?sessionId=${SESSION_ID}`, { name, scope, content, kind });
|
|
6284
|
+
addStatusMessage(`技能 ${kind === 'skill' ? '🧠 ' : '/'}${name} 已保存`);
|
|
6285
|
+
sklView('list'); loadSkills();
|
|
6286
|
+
} catch (e) { addStatusMessage('保存失败:' + e.message); }
|
|
6287
|
+
};
|
|
6288
|
+
window.deleteSkill = async function(name, source, kind) {
|
|
6289
|
+
if (!confirm(`确定删除技能「${name}」?`)) return;
|
|
6290
|
+
try {
|
|
6291
|
+
await sklJson(`/api/skills?sessionId=${SESSION_ID}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, scope: source, kind }) });
|
|
6292
|
+
addStatusMessage(`技能「${name}」已删除`);
|
|
6293
|
+
refreshSkillList(); loadSkills();
|
|
6294
|
+
} catch (e) { addStatusMessage('删除失败:' + e.message); }
|
|
6295
|
+
};
|
|
6296
|
+
|
|
6297
|
+
// ── ✨ 从本次任务生成:交给当前任务的 AI(它最清楚刚才怎么做成的),先出草稿、确认后才写文件 ──
|
|
6298
|
+
window.genSkillFromTask = function() {
|
|
6299
|
+
if (currentSession?.mode !== 'agent') return addStatusMessage('只有 AI 任务里可以生成技能');
|
|
6300
|
+
if (workState !== 'idle') return addStatusMessage('AI 正在干活,等这一轮结束再生成技能');
|
|
6301
|
+
const hint = document.getElementById('skl-gen-hint').value.trim();
|
|
6302
|
+
const kind = document.querySelector('input[name=skl-gen-kind]:checked').value;
|
|
6303
|
+
const scope = document.querySelector('input[name=skl-gen-scope]:checked').value;
|
|
6304
|
+
const dirs = _sklDirs?.[scope];
|
|
6305
|
+
const dir = dirs ? (kind === 'skill' ? dirs.skill : dirs.command) : (scope === 'project' ? (kind === 'skill' ? '.claude/skills' : '.claude/commands') : (kind === 'skill' ? '~/.claude/skills' : '~/.claude/commands'));
|
|
6306
|
+
const fmt = kind === 'skill'
|
|
6307
|
+
? `类型:Agent 技能(AI 遇到合适场景会自己调用)。在 ${dir}/<名称>/ 下建 SKILL.md:开头 YAML frontmatter 写 name(同目录名)和 description(用中文写清楚「什么场景该用这个技能」,AI 靠它判断);正文写目标、分步骤做法、注意事项、完成标准。需要辅助脚本就放在同一目录,并在 SKILL.md 里写明怎么用。`
|
|
6308
|
+
: `类型:斜杠命令(用户点按钮或输入 /名称 运行)。文件 ${dir}/<名称>.md:第一行是「# 中文标题」(myhi 拿它当说明),正文写目标、前提、分步骤做法、注意事项、完成标准;每次运行需要用户补充的内容用 $ARGUMENTS 表示。`;
|
|
6309
|
+
const prompt = [
|
|
6310
|
+
`请把本次会话里我们完成的工作提炼成一个可以复用的 Claude Code 技能${hint ? ',我的想法:' + hint : '(你判断最值得沉淀的那套做法)'}。`,
|
|
6311
|
+
fmt,
|
|
6312
|
+
kind === 'skill' ? '名称只能用小写字母、数字和连字符(如 deploy-test,不超过 64 个字符,不能含 anthropic / claude),目录名和 SKILL.md 里的 name 要一致。' : '名称用小写英文和连字符(如 deploy-test)。',
|
|
6313
|
+
'把本项目特有的路径、名字、账号改成参数或说明,让它在别的项目、别的电脑上也能用;不要写入任何密码、token、密钥或客户的真实敏感信息。',
|
|
6314
|
+
'先把完整草稿(含名称和每个文件的内容)贴出来问我是否保存(可以直接保存 / 我要修改);我确认之后再写入文件。在我确认之前不要创建或修改任何文件。如果同名技能已经存在,先告诉我。',
|
|
6315
|
+
].join('\n');
|
|
6316
|
+
closeSkillSheet();
|
|
6317
|
+
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
6318
|
+
const display = `✨ 把本次任务的做法生成${kind === 'skill' ? '自动' : '手动'}技能(${scope === 'project' ? '本项目' : '本机所有项目'})${hint ? ':' + hint : ''}`;
|
|
6319
|
+
addInputMessage(display);
|
|
6320
|
+
socket.emit('agent:query', { prompt, display });
|
|
6321
|
+
showThinking();
|
|
6322
|
+
_sklGenPending = scope;
|
|
6323
|
+
};
|
|
6324
|
+
let _sklGenPending = false; // 生成中(存范围):这轮结束刷新快捷栏的技能按钮,并让相关任务重新加载技能
|
|
6325
|
+
|
|
6326
|
+
// ── 📤 分享 ──
|
|
6327
|
+
window.openSkillShare = function(name, source, kind) {
|
|
6328
|
+
_sklShare = { name, source, kind, force: false };
|
|
6329
|
+
sklView('share');
|
|
6330
|
+
document.getElementById('skl-share-info').innerHTML = `<b>${kind === 'skill' ? '🧠 ' : '/'}${escHtml(name)}</b> <span class="skl-tag ${kind}">${sklKindLabel(kind)}</span><span class="skl-tag">${sklScopeLabel(source)}</span>`;
|
|
6331
|
+
document.getElementById('skl-share-warn').style.display = 'none';
|
|
6332
|
+
document.getElementById('skl-share-out').style.display = 'none';
|
|
6333
|
+
document.getElementById('skl-pub-form').style.display = 'none';
|
|
6334
|
+
};
|
|
6335
|
+
function sklShareFail(e, retry) {
|
|
6336
|
+
const w = document.getElementById('skl-share-warn');
|
|
6337
|
+
if (e.data?.secrets) {
|
|
6338
|
+
w.innerHTML = `⚠️ ${escHtml(e.message)}<div style="margin-top:0.35rem;display:flex;gap:0.4rem"><button class="dr-btn" onclick="editSkill('${sklEsc(_sklShare.name)}','${_sklShare.source}','${_sklShare.kind}')">去编辑</button><button class="dr-btn" id="skl-force-btn">确认没有敏感信息,继续</button></div>`;
|
|
6339
|
+
w.style.display = '';
|
|
6340
|
+
document.getElementById('skl-force-btn').onclick = () => { _sklShare.force = true; w.style.display = 'none'; retry(); };
|
|
6341
|
+
} else addStatusMessage('分享失败:' + e.message);
|
|
6342
|
+
}
|
|
6343
|
+
const sklShareBody = () => ({ sessionId: SESSION_ID, name: _sklShare.name, source: _sklShare.source, kind: _sklShare.kind, force: _sklShare.force });
|
|
6344
|
+
function sklOut(html) { const o = document.getElementById('skl-share-out'); o.innerHTML = html; o.style.display = ''; }
|
|
6345
|
+
async function sklCopy(text, ok) {
|
|
6346
|
+
try { await navigator.clipboard.writeText(text); addStatusMessage(ok); }
|
|
6347
|
+
catch { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); addStatusMessage(ok); } catch {} ta.remove(); }
|
|
6348
|
+
}
|
|
6349
|
+
window.sklShareDownload = async function() {
|
|
6350
|
+
try {
|
|
6351
|
+
const d = await sklPost('/api/skills/export', sklShareBody());
|
|
6352
|
+
const blob = new Blob([JSON.stringify(d.pkg, null, 2)], { type: 'application/json' });
|
|
6353
|
+
const a = document.createElement('a');
|
|
6354
|
+
a.href = URL.createObjectURL(blob); a.download = d.pkg.name.replace(/:/g, '-') + '.myhiskill';
|
|
6355
|
+
document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000);
|
|
6356
|
+
sklOut('✓ 已下载 <b>' + escHtml(a.download) + '</b>。发给对方后,在对方 myhi 的「技能 → 📥 导入」里选择这个文件。');
|
|
6357
|
+
} catch (e) { sklShareFail(e, sklShareDownload); }
|
|
6358
|
+
};
|
|
6359
|
+
window.sklShareCode = async function() {
|
|
6360
|
+
try {
|
|
6361
|
+
const d = await sklPost('/api/skills/export', sklShareBody());
|
|
6362
|
+
await sklCopy(d.code, '分享码已复制');
|
|
6363
|
+
sklOut(`✓ 分享码已复制(${d.code.length} 字符)。对方在「技能 → 📥 导入」里粘贴即可。<textarea class="slash-inp" rows="3" readonly style="width:100%;box-sizing:border-box;margin-top:0.35rem;font-size:0.7rem" onclick="this.select()">${escHtml(d.code)}</textarea>`);
|
|
6364
|
+
} catch (e) { sklShareFail(e, sklShareCode); }
|
|
6365
|
+
};
|
|
6366
|
+
window.sklShareLink = async function() {
|
|
6367
|
+
try {
|
|
6368
|
+
const d = await sklPost('/api/skills/share-link', sklShareBody());
|
|
6369
|
+
await sklCopy(d.url, '分享链接已复制');
|
|
6370
|
+
sklOut(`✓ 链接已复制,<b>${new Date(d.expires).toLocaleString()}</b> 前有效。对方在「📥 导入」里粘贴(两台 myhi 要能互相访问):<input class="slash-inp" readonly value="${sklEsc(d.url)}" style="width:100%;box-sizing:border-box;margin-top:0.35rem;font-size:0.72rem" onclick="this.select()">`);
|
|
6371
|
+
} catch (e) { sklShareFail(e, sklShareLink); }
|
|
6372
|
+
};
|
|
6373
|
+
window.sklSharePublishForm = function() {
|
|
6374
|
+
const f = document.getElementById('skl-pub-form');
|
|
6375
|
+
f.style.display = f.style.display === 'none' ? '' : 'none';
|
|
6376
|
+
const last = JSON.parse(localStorage.getItem('myhi_skl_pub') || 'null') || (_sklSubs[0] ? { url: _sklSubs[0].url, username: _sklSubs[0].username, branch: _sklSubs[0].branch } : {});
|
|
6377
|
+
if (!document.getElementById('skl-pub-url').value) {
|
|
6378
|
+
document.getElementById('skl-pub-url').value = last.url || '';
|
|
6379
|
+
document.getElementById('skl-pub-user').value = last.username || '';
|
|
6380
|
+
document.getElementById('skl-pub-branch').value = last.branch || '';
|
|
6381
|
+
}
|
|
6382
|
+
};
|
|
6383
|
+
window.sklPublish = async function() {
|
|
6384
|
+
const url = document.getElementById('skl-pub-url').value.trim();
|
|
6385
|
+
if (!url) { addStatusMessage('请填写团队仓库地址'); return; }
|
|
6386
|
+
const body = { ...sklShareBody(), url, username: document.getElementById('skl-pub-user').value.trim(), password: document.getElementById('skl-pub-pass').value, branch: document.getElementById('skl-pub-branch').value.trim() };
|
|
6387
|
+
sklOut('发布中…(克隆仓库、提交、推送)');
|
|
6388
|
+
try {
|
|
6389
|
+
const d = await sklPost('/api/skills/publish', body);
|
|
6390
|
+
localStorage.setItem('myhi_skl_pub', JSON.stringify({ url, username: body.username, branch: body.branch }));
|
|
6391
|
+
sklOut(d.unchanged ? '仓库里已经是这个版本,不用重复发布。' : '✓ 已发布到团队仓库。订阅了这个仓库的 myhi「检查更新」时会看到。');
|
|
6392
|
+
} catch (e) { document.getElementById('skl-share-out').style.display = 'none'; sklShareFail(e, sklPublish); }
|
|
6125
6393
|
};
|
|
6126
6394
|
|
|
6127
|
-
|
|
6128
|
-
|
|
6395
|
+
// ── 📥 导入:分享码 / 分享链接 / 文件 → 预览 + 安全扫描 → 选范围安装 ──
|
|
6396
|
+
window.sklImportFile = async function(file) {
|
|
6397
|
+
if (!file) return;
|
|
6398
|
+
const text = await file.text();
|
|
6399
|
+
let body;
|
|
6400
|
+
if (/\.md$/i.test(file.name)) {
|
|
6401
|
+
// 直接给了一个 .md:按手动技能处理,名称取文件名
|
|
6402
|
+
const name = file.name.replace(/\.md$/i, '').replace(/[^\w.-]+/g, '-');
|
|
6403
|
+
body = { pkg: { myhiSkill: 1, kind: 'command', name, files: [{ path: name + '.md', content: text }] } };
|
|
6404
|
+
} else if (text.trim().startsWith('MYHI-SKILL:')) body = { code: text };
|
|
6405
|
+
else body = { pkg: text };
|
|
6406
|
+
sklImportPreview(body);
|
|
6407
|
+
};
|
|
6408
|
+
window.sklImportPreview = async function(body) {
|
|
6409
|
+
const box = document.getElementById('skl-imp-preview');
|
|
6410
|
+
if (!body) {
|
|
6411
|
+
const t = document.getElementById('skl-imp-text').value.trim();
|
|
6412
|
+
if (!t) { addStatusMessage('先粘贴分享码 / 分享链接,或选择技能文件'); return; }
|
|
6413
|
+
body = /^https?:\/\/\S+\/s\/skill\/[\w-]+/.test(t) ? { url: t.match(/https?:\/\/\S+\/s\/skill\/[\w-]+/)[0] } : { code: t };
|
|
6414
|
+
}
|
|
6415
|
+
box.innerHTML = '<div class="skl-empty">读取中…</div>';
|
|
6129
6416
|
try {
|
|
6130
|
-
const
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6417
|
+
const d = await sklPost('/api/skills/preview', { sessionId: SESSION_ID, ...body });
|
|
6418
|
+
_sklImp = d;
|
|
6419
|
+
const p = d.pkg;
|
|
6420
|
+
const danger = d.findings.filter(f => f.level === 'danger'), warn = d.findings.filter(f => f.level === 'warn');
|
|
6421
|
+
box.innerHTML = `
|
|
6422
|
+
<div class="skl-card">
|
|
6423
|
+
<div><b>${p.kind === 'skill' ? '🧠 ' : '/'}${escHtml(p.name)}</b> <span class="skl-tag ${p.kind}">${sklKindLabel(p.kind)}</span>${p.from ? `<span class="skl-tag">来自 ${escHtml(p.from)}</span>` : ''}</div>
|
|
6424
|
+
<div class="skl-desc" style="margin-top:0.2rem">${escHtml(p.desc || '(没有说明)')}</div>
|
|
6425
|
+
${p.renamedFrom ? `<div class="skl-hint" style="color:var(--yellow)">原名「${escHtml(p.renamedFrom)}」不符合自动技能的命名规范,安装时改成「${escHtml(p.name)}」</div>` : ''}
|
|
6426
|
+
</div>
|
|
6427
|
+
${sklFindingsHtml(d.findings)}
|
|
6428
|
+
<details class="skl-files"><summary>查看全部内容(${p.files.length} 个文件)——安装前建议过一遍</summary>
|
|
6429
|
+
${p.files.map(f => `<div class="skl-fname">${escHtml(f.path)}</div><pre>${escHtml(f.content)}</pre>`).join('')}
|
|
6430
|
+
</details>
|
|
6431
|
+
<div class="skl-row" style="margin-top:0.5rem">
|
|
6432
|
+
<select id="skl-imp-scope" class="slash-inp skl-sel" onchange="sklImpExists()">
|
|
6433
|
+
<option value="project">只装到本项目</option>
|
|
6434
|
+
<option value="user">装到本机所有项目</option>
|
|
6435
|
+
</select>
|
|
6436
|
+
</div>
|
|
6437
|
+
<div id="skl-imp-exists" class="skl-hint"></div>
|
|
6438
|
+
${danger.length ? '<label class="skl-confirm"><input type="checkbox" id="skl-imp-ack"> 我已看过上面标红的内容,确认这个技能来源可信</label>' : ''}
|
|
6439
|
+
<button class="action-sheet-cancel" style="background:${danger.length ? 'var(--red)' : 'var(--green)'};color:#fff;font-weight:600;margin-top:0.4rem" onclick="sklInstall()">${danger.length ? '⚠️ 仍然安装' : '安装'}</button>`;
|
|
6440
|
+
// 带脚本的技能默认只装本项目
|
|
6441
|
+
document.getElementById('skl-imp-scope').value = p.kind === 'skill' && p.files.length > 1 ? 'project' : (warn.length || danger.length ? 'project' : 'user');
|
|
6442
|
+
sklImpExists();
|
|
6140
6443
|
} catch (e) {
|
|
6141
|
-
|
|
6444
|
+
box.innerHTML = `<div class="skl-warn">⚠️ ${escHtml(e.message)}</div>`;
|
|
6142
6445
|
}
|
|
6143
6446
|
};
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
window.
|
|
6149
|
-
|
|
6150
|
-
document.getElementById('
|
|
6151
|
-
document.getElementById('
|
|
6152
|
-
document.getElementById('skill-close-btn').style.display = 'none';
|
|
6153
|
-
document.getElementById('skill-sheet-title').textContent = '从 Git 安装技能';
|
|
6154
|
-
document.getElementById('skill-import-preview').style.display = 'none';
|
|
6155
|
-
document.getElementById('skill-import-install-btn').style.display = 'none';
|
|
6156
|
-
document.getElementById('skill-import-scan-btn').innerHTML = '';
|
|
6157
|
-
document.getElementById('skill-git-url').value = '';
|
|
6158
|
-
_gitSkills = [];
|
|
6159
|
-
setTimeout(() => document.getElementById('skill-git-url').focus(), 100);
|
|
6447
|
+
function sklFindingsHtml(findings) {
|
|
6448
|
+
if (!findings?.length) return '<div class="skl-ok">✓ 安全检查没发现可疑写法</div>';
|
|
6449
|
+
return '<div class="skl-findings">' + findings.map(f => `<div class="skl-f ${f.level}">${f.level === 'danger' ? '⛔' : '⚠️'} ${escHtml(f.why)}${f.file ? ` <small>${escHtml(f.file)}${f.line ? ' 第 ' + f.line + ' 行' : ''}</small>` : ''}${f.text ? `<code>${escHtml(f.text)}</code>` : ''}</div>`).join('') + '</div>';
|
|
6450
|
+
}
|
|
6451
|
+
window.sklImpExists = function() {
|
|
6452
|
+
if (!_sklImp) return;
|
|
6453
|
+
const sc = document.getElementById('skl-imp-scope').value;
|
|
6454
|
+
document.getElementById('skl-imp-exists').textContent = _sklImp.exists?.[sc] ? `⚠️ ${sc === 'project' ? '本项目' : '本机'}已经有同名技能,安装会覆盖它` : '';
|
|
6160
6455
|
};
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6456
|
+
window.sklInstall = async function() {
|
|
6457
|
+
if (!_sklImp) return;
|
|
6458
|
+
const ack = document.getElementById('skl-imp-ack');
|
|
6459
|
+
if (ack && !ack.checked) { addStatusMessage('这个技能里有标红的可疑内容:确认来源可信后先勾选确认'); return; }
|
|
6460
|
+
const scope = document.getElementById('skl-imp-scope').value;
|
|
6461
|
+
try {
|
|
6462
|
+
await sklPost('/api/skills/install', { sessionId: SESSION_ID, pkg: _sklImp.pkg, scope });
|
|
6463
|
+
addStatusMessage(`已安装技能「${_sklImp.pkg.name}」(${scope === 'project' ? '本项目' : '本机所有项目'}),下一轮 AI 就能用`);
|
|
6464
|
+
_sklImp = null; document.getElementById('skl-imp-text').value = '';
|
|
6465
|
+
sklView('list'); loadSkills();
|
|
6466
|
+
} catch (e) { addStatusMessage('安装失败:' + e.message); }
|
|
6467
|
+
};
|
|
6468
|
+
|
|
6469
|
+
// ── 👥 团队仓库:订阅列表 / 检查更新 / 扫描安装 ──
|
|
6470
|
+
async function loadSubs() { try { _sklSubs = (await sklJson('/api/skills/subs')).subs || []; } catch { _sklSubs = []; } updateSubBadge(); return _sklSubs; }
|
|
6471
|
+
function updateSubBadge() {
|
|
6472
|
+
const n = _sklSubs.reduce((a, s) => a + (s.updates?.length || 0), 0);
|
|
6473
|
+
const b = document.getElementById('skl-upd-badge'), ban = document.getElementById('skl-upd-banner');
|
|
6474
|
+
if (b) { b.textContent = n; b.style.display = n ? '' : 'none'; }
|
|
6475
|
+
if (ban) { ban.textContent = `👥 团队仓库有 ${n} 个技能更新 / 新技能 · 点这里查看`; ban.style.display = n ? '' : 'none'; }
|
|
6476
|
+
const skBtn = document.querySelector('#sk-claude-agent .sk[data-sk="skills"]');
|
|
6477
|
+
if (skBtn) skBtn.classList.toggle('skl-dot', n > 0);
|
|
6478
|
+
}
|
|
6479
|
+
let _sklAutoChecked = false;
|
|
6480
|
+
async function sklCheckSubsQuietly() {
|
|
6481
|
+
await loadSubs();
|
|
6482
|
+
if (_sklAutoChecked) return;
|
|
6483
|
+
_sklAutoChecked = true;
|
|
6484
|
+
for (const s of _sklSubs) {
|
|
6485
|
+
if (Date.now() - (s.lastCheck || 0) < 6 * 3600 * 1000) continue; // 6 小时内查过就不再查
|
|
6486
|
+
try { await sklPost(`/api/skills/subs/${s.id}/check`); } catch {}
|
|
6487
|
+
}
|
|
6488
|
+
await loadSubs();
|
|
6489
|
+
}
|
|
6490
|
+
const maskUrl = (u) => String(u || '').replace(/\/\/[^@/]+@/, '//');
|
|
6491
|
+
async function renderSubs() {
|
|
6492
|
+
const box = document.getElementById('skl-subs');
|
|
6493
|
+
await loadSubs();
|
|
6494
|
+
box.innerHTML = _sklSubs.length ? '<div class="skl-lbl">已订阅</div>' + _sklSubs.map(s => `
|
|
6495
|
+
<div class="skl-card skl-sub">
|
|
6496
|
+
<div style="display:flex;align-items:center;gap:0.4rem">
|
|
6497
|
+
<div style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${sklEsc(maskUrl(s.url))}">${escHtml(maskUrl(s.url).replace(/^https?:\/\//, ''))}</div>
|
|
6498
|
+
<span class="skl-tag">${s.scope === 'project' ? '本项目' : '本机'}</span>
|
|
6499
|
+
</div>
|
|
6500
|
+
<div class="skl-desc">已装 ${Object.keys(s.installed || {}).length} 个 · ${s.lastCheck ? '上次检查 ' + new Date(s.lastCheck).toLocaleString() : '还没检查过'}${s.updates?.length ? ` · <b style="color:var(--green)">${s.updates.length} 个更新</b>` : ''}</div>
|
|
6501
|
+
<div class="skl-row" style="margin-top:0.35rem">
|
|
6502
|
+
<button class="dr-btn take" onclick="sklSubCheck('${s.id}')">检查更新</button>
|
|
6503
|
+
<button class="dr-btn" style="margin-left:auto" onclick="sklSubRemove('${s.id}')">取消订阅</button>
|
|
6504
|
+
</div>
|
|
6505
|
+
<div id="skl-sub-out-${s.id}"></div>
|
|
6506
|
+
</div>`).join('') : '<div class="skl-hint">还没有订阅团队仓库。团队把技能放在一个 Git 仓库里(commands/、skills/ 目录),大家订阅后就能一起用、收到更新;也可以在技能的「📤 分享」里发布到仓库。</div>';
|
|
6507
|
+
}
|
|
6508
|
+
function sklPackListHtml(list, withStatus) {
|
|
6509
|
+
return list.map((s, i) => `
|
|
6510
|
+
<label class="skl-pick">
|
|
6511
|
+
<input type="checkbox" ${s.findings?.some(f => f.level === 'danger') ? '' : 'checked'} data-key="${sklEsc(s.kind + ':' + s.name)}">
|
|
6512
|
+
<div style="min-width:0;flex:1">
|
|
6513
|
+
<div><span class="skl-name">${s.kind === 'skill' ? '🧠 ' : '/'}${escHtml(s.name)}</span><span class="skl-tag ${s.kind}">${sklKindLabel(s.kind)}</span>${withStatus ? `<span class="skl-tag ${s.status === 'new' ? 'skill' : ''}">${s.status === 'new' ? '新技能' : '有更新'}</span>` : ''}</div>
|
|
6514
|
+
<div class="skl-desc">${escHtml(s.desc || '')}</div>
|
|
6515
|
+
${s.findings?.length ? sklFindingsHtml(s.findings) : ''}
|
|
6516
|
+
</div>
|
|
6517
|
+
</label>`).join('');
|
|
6518
|
+
}
|
|
6519
|
+
window.sklSubCheck = async function(id) {
|
|
6520
|
+
const out = document.getElementById('skl-sub-out-' + id);
|
|
6521
|
+
out.innerHTML = '<div class="skl-empty">检查中…</div>';
|
|
6522
|
+
try {
|
|
6523
|
+
const d = await sklPost(`/api/skills/subs/${id}/check`);
|
|
6524
|
+
await loadSubs();
|
|
6525
|
+
if (!d.updates.length) { out.innerHTML = '<div class="skl-ok">✓ 已是最新</div>'; return; }
|
|
6526
|
+
out.innerHTML = sklPackListHtml(d.updates, true) + `<button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-top:0.4rem" onclick="sklSubUpdate('${id}')">更新 / 安装选中的</button>`;
|
|
6527
|
+
} catch (e) { out.innerHTML = `<div class="skl-warn">⚠️ ${escHtml(e.message)}</div>`; }
|
|
6528
|
+
};
|
|
6529
|
+
window.sklSubUpdate = async function(id) {
|
|
6530
|
+
const names = [...document.querySelectorAll(`#skl-sub-out-${id} input[type=checkbox]:checked`)].map(c => c.dataset.key);
|
|
6531
|
+
if (!names.length) { addStatusMessage('请至少选一个'); return; }
|
|
6532
|
+
try {
|
|
6533
|
+
const d = await sklPost('/api/skills/repo/install', { sessionId: SESSION_ID, subId: id, names });
|
|
6534
|
+
addStatusMessage(`已更新 ${d.installed.length} 个技能`);
|
|
6535
|
+
loadSkills(); renderSubs();
|
|
6536
|
+
} catch (e) { addStatusMessage('更新失败:' + e.message); }
|
|
6537
|
+
};
|
|
6538
|
+
window.sklSubRemove = async function(id) {
|
|
6539
|
+
if (!confirm('取消订阅这个仓库?已经装好的技能会保留。')) return;
|
|
6540
|
+
try { await sklJson(`/api/skills/subs/${id}`, { method: 'DELETE' }); } catch {}
|
|
6541
|
+
renderSubs();
|
|
6542
|
+
};
|
|
6543
|
+
document.getElementById('skill-git-url')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); scanGitSkills(); } });
|
|
6166
6544
|
window.scanGitSkills = async function() {
|
|
6167
6545
|
const url = document.getElementById('skill-git-url').value.trim();
|
|
6168
6546
|
if (!url) { addStatusMessage('请输入 Git 仓库地址'); return; }
|
|
6169
|
-
const
|
|
6170
|
-
|
|
6171
|
-
document.getElementById('skill-import-preview').style.display = 'none';
|
|
6172
|
-
document.getElementById('skill-import-install-btn').style.display = 'none';
|
|
6547
|
+
const box = document.getElementById('skill-import-preview');
|
|
6548
|
+
box.innerHTML = '<div class="skl-empty">正在克隆并扫描…</div>';
|
|
6173
6549
|
try {
|
|
6174
|
-
const
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
const data = await resp.json();
|
|
6182
|
-
statusEl.innerHTML = '';
|
|
6183
|
-
if (data.error) { addStatusMessage(data.error); return; }
|
|
6184
|
-
_gitSkills = data.skills || [];
|
|
6185
|
-
if (!_gitSkills.length) {
|
|
6186
|
-
addStatusMessage(data.message || '未找到技能文件');
|
|
6187
|
-
return;
|
|
6188
|
-
}
|
|
6189
|
-
const preview = document.getElementById('skill-import-preview');
|
|
6190
|
-
preview.innerHTML = _gitSkills.map((s, i) => `
|
|
6191
|
-
<label style="display:flex;align-items:flex-start;gap:0.5rem;padding:0.4rem 0;border-bottom:1px solid var(--surface2);cursor:pointer">
|
|
6192
|
-
<input type="checkbox" checked data-skill-idx="${i}" style="margin-top:0.15rem">
|
|
6193
|
-
<div style="min-width:0;flex:1">
|
|
6194
|
-
<div style="color:var(--purple2);font-weight:500">/${escHtml(s.name)}</div>
|
|
6195
|
-
<div style="color:var(--muted);font-size:0.72rem">${escHtml(s.desc)}</div>
|
|
6196
|
-
</div>
|
|
6197
|
-
</label>
|
|
6198
|
-
`).join('');
|
|
6199
|
-
preview.style.display = '';
|
|
6200
|
-
document.getElementById('skill-import-install-btn').style.display = '';
|
|
6201
|
-
} catch (e) {
|
|
6202
|
-
addStatusMessage('扫描失败: ' + e.message);
|
|
6203
|
-
statusEl.innerHTML = '';
|
|
6204
|
-
}
|
|
6550
|
+
const d = await sklPost('/api/skills/repo/scan', { url, username: document.getElementById('skill-git-user').value.trim() || undefined, password: document.getElementById('skill-git-pass').value || undefined });
|
|
6551
|
+
_gitSkills = d.skills || [];
|
|
6552
|
+
if (!_gitSkills.length) { box.innerHTML = '<div class="skl-hint">这个仓库里没找到技能(commands/*.md、skills/*/SKILL.md)</div>'; return; }
|
|
6553
|
+
box.innerHTML = sklPackListHtml(_gitSkills, false) +
|
|
6554
|
+
'<label class="skl-confirm"><input type="checkbox" id="skl-git-sub" checked> 订阅这个仓库:以后有更新 / 新技能时提示我</label>' +
|
|
6555
|
+
'<button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-top:0.4rem" onclick="installGitSkills()">安装选中的技能</button>';
|
|
6556
|
+
} catch (e) { box.innerHTML = `<div class="skl-warn">⚠️ ${escHtml(e.message)}</div>`; }
|
|
6205
6557
|
};
|
|
6206
|
-
|
|
6207
6558
|
window.installGitSkills = async function() {
|
|
6208
|
-
const
|
|
6209
|
-
|
|
6210
|
-
const
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
method: 'POST',
|
|
6223
|
-
headers: { 'Content-Type': 'application/json' },
|
|
6224
|
-
body: JSON.stringify({ name: s.name, scope, content: s.content }),
|
|
6225
|
-
});
|
|
6226
|
-
const data = await resp.json();
|
|
6227
|
-
if (data.error) fail++;
|
|
6228
|
-
else ok++;
|
|
6229
|
-
} catch { fail++; }
|
|
6230
|
-
}
|
|
6231
|
-
addStatusMessage(`安装完成: ${ok} 个成功${fail ? `, ${fail} 个失败` : ''}`);
|
|
6232
|
-
btn.textContent = '安装选中的技能';
|
|
6233
|
-
btn.disabled = false;
|
|
6234
|
-
backToSkillList();
|
|
6235
|
-
loadSkills();
|
|
6559
|
+
const names = [...document.querySelectorAll('#skill-import-preview input[data-key]:checked')].map(c => c.dataset.key);
|
|
6560
|
+
if (!names.length) { addStatusMessage('请至少选择一个技能'); return; }
|
|
6561
|
+
const dangerSel = _gitSkills.filter(s => names.includes(s.kind + ':' + s.name) && s.findings?.some(f => f.level === 'danger'));
|
|
6562
|
+
if (dangerSel.length && !confirm(`选中的技能里有 ${dangerSel.length} 个带标红的可疑内容(${dangerSel.map(s => s.name).join('、')})。确认来源可信、仍然安装?`)) return;
|
|
6563
|
+
try {
|
|
6564
|
+
const d = await sklPost('/api/skills/repo/install', {
|
|
6565
|
+
sessionId: SESSION_ID, names, scope: document.getElementById('skill-import-scope').value,
|
|
6566
|
+
url: document.getElementById('skill-git-url').value.trim(), username: document.getElementById('skill-git-user').value.trim() || undefined,
|
|
6567
|
+
password: document.getElementById('skill-git-pass').value || undefined, subscribe: document.getElementById('skl-git-sub')?.checked,
|
|
6568
|
+
});
|
|
6569
|
+
addStatusMessage(`已安装 ${d.installed.length} 个技能${d.sub ? ',并订阅了这个仓库' : ''}`);
|
|
6570
|
+
document.getElementById('skill-import-preview').innerHTML = '';
|
|
6571
|
+
loadSkills(); renderSubs();
|
|
6572
|
+
} catch (e) { addStatusMessage('安装失败:' + e.message); }
|
|
6236
6573
|
};
|
|
6237
6574
|
|
|
6238
|
-
// ──
|
|
6575
|
+
// ── 🛒 技能市场(社区仓库,走团队仓库的扫描安装,默认不订阅)──
|
|
6239
6576
|
const SKILL_MARKET = [
|
|
6240
|
-
{ name: 'Claude Command Suite', desc: '216+ 命令, 12 技能, 54 AI Agent — 全面的软件工程工作流', repo: 'qdhenry/Claude-Command-Suite', stars: '1k+'
|
|
6241
|
-
{ name: 'Production Commands', desc: '全栈开发、安全、数据/ML、DevOps 命令集', repo: 'wshobson/commands', stars: '500+'
|
|
6242
|
-
{ name: 'Claude Slash Commands', desc: '轻量级常用命令集合,直接可用', repo: 'artemgetmann/claude-slash-commands', stars: '200+'
|
|
6243
|
-
{ name: 'Claude Commands', desc: '自定义命令合集,附完整文档', repo: 'david-t-martel/claude-commands', stars: '100+'
|
|
6244
|
-
{ name: 'Claude Code Skills', desc: '专业技能市场,生产级 Skill 合集', repo: 'daymade/claude-code-skills', stars: '100+'
|
|
6245
|
-
{ name: 'My Claude Code', desc: '日常使用的精选命令和工作流', repo: 'vincenthopf/My-Claude-Code', stars: '50+'
|
|
6577
|
+
{ name: 'Claude Command Suite', desc: '216+ 命令, 12 技能, 54 AI Agent — 全面的软件工程工作流', repo: 'qdhenry/Claude-Command-Suite', stars: '1k+' },
|
|
6578
|
+
{ name: 'Production Commands', desc: '全栈开发、安全、数据/ML、DevOps 命令集', repo: 'wshobson/commands', stars: '500+' },
|
|
6579
|
+
{ name: 'Claude Slash Commands', desc: '轻量级常用命令集合,直接可用', repo: 'artemgetmann/claude-slash-commands', stars: '200+' },
|
|
6580
|
+
{ name: 'Claude Commands', desc: '自定义命令合集,附完整文档', repo: 'david-t-martel/claude-commands', stars: '100+' },
|
|
6581
|
+
{ name: 'Claude Code Skills', desc: '专业技能市场,生产级 Skill 合集', repo: 'daymade/claude-code-skills', stars: '100+' },
|
|
6582
|
+
{ name: 'My Claude Code', desc: '日常使用的精选命令和工作流', repo: 'vincenthopf/My-Claude-Code', stars: '50+' },
|
|
6246
6583
|
];
|
|
6247
|
-
|
|
6248
6584
|
window.showSkillMarket = function() {
|
|
6249
|
-
|
|
6250
|
-
document.getElementById('skill-
|
|
6251
|
-
document.getElementById('skill-import-view').style.display = 'none';
|
|
6252
|
-
document.getElementById('skill-market-view').style.display = '';
|
|
6253
|
-
document.getElementById('skill-close-btn').style.display = 'none';
|
|
6254
|
-
document.getElementById('skill-sheet-title').textContent = '技能市场';
|
|
6255
|
-
|
|
6256
|
-
const list = document.getElementById('skill-market-list');
|
|
6257
|
-
list.innerHTML = '<div style="font-size:0.72rem;color:var(--muted);padding:0.3rem 0.5rem;border-bottom:1px solid var(--surface2)">热门社区技能仓库 · 点击安装</div>' +
|
|
6585
|
+
sklView('market');
|
|
6586
|
+
document.getElementById('skill-market-list').innerHTML = '<div class="skl-hint">热门社区技能仓库 · 点一个扫描,选择要装的(安装前会做安全检查)</div>' +
|
|
6258
6587
|
SKILL_MARKET.map((m, i) => `
|
|
6259
|
-
<div class="
|
|
6260
|
-
<div
|
|
6588
|
+
<div class="skl-item" style="cursor:pointer" onclick="installFromMarket(${i})">
|
|
6589
|
+
<div class="skl-main">
|
|
6261
6590
|
<div style="font-weight:500">${escHtml(m.name)} <span style="color:var(--muted);font-size:0.7rem">★ ${m.stars}</span></div>
|
|
6262
|
-
<div class="desc">${escHtml(m.desc)}</div>
|
|
6591
|
+
<div class="skl-desc">${escHtml(m.desc)}</div>
|
|
6263
6592
|
<div style="color:var(--blue);font-size:0.68rem">${escHtml(m.repo)}</div>
|
|
6264
6593
|
</div>
|
|
6265
|
-
</div
|
|
6266
|
-
|
|
6267
|
-
'<div style="padding:0.6rem 0.5rem;font-size:0.72rem;color:var(--muted);border-top:1px solid var(--surface2)">' +
|
|
6268
|
-
'更多: <a href="https://github.com/hesreallyhim/awesome-claude-code" target="_blank" style="color:var(--blue)">awesome-claude-code</a>' +
|
|
6269
|
-
' · <a href="https://skillsmp.com" target="_blank" style="color:var(--blue)">skillsmp.com</a>' +
|
|
6270
|
-
' · <a href="https://www.skillhub.club" target="_blank" style="color:var(--blue)">skillhub.club</a></div>';
|
|
6594
|
+
</div>`).join('') +
|
|
6595
|
+
'<div class="skl-hint" style="margin-top:0.5rem">更多:<a href="https://github.com/hesreallyhim/awesome-claude-code" target="_blank" style="color:var(--blue)">awesome-claude-code</a></div>';
|
|
6271
6596
|
};
|
|
6272
|
-
|
|
6273
6597
|
window.installFromMarket = async function(idx) {
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
// 复用 Git 安装流程
|
|
6277
|
-
showSkillImport();
|
|
6278
|
-
document.getElementById('skill-git-url').value = url;
|
|
6279
|
-
// 自动触发扫描
|
|
6598
|
+
sklView('team');
|
|
6599
|
+
document.getElementById('skill-git-url').value = `https://github.com/${SKILL_MARKET[idx].repo}.git`;
|
|
6280
6600
|
await scanGitSkills();
|
|
6601
|
+
const sub = document.getElementById('skl-git-sub'); if (sub) sub.checked = false;
|
|
6281
6602
|
};
|
|
6282
6603
|
|
|
6283
6604
|
// ── Git 提交推送 ──────────────────────────────
|
|
@@ -6500,7 +6821,7 @@
|
|
|
6500
6821
|
].join('');
|
|
6501
6822
|
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
6502
6823
|
addInputMessage('📘 让 AI 生成项目说明');
|
|
6503
|
-
socket.emit('agent:query', { prompt });
|
|
6824
|
+
socket.emit('agent:query', { prompt, display: '📘 让 AI 生成项目说明' });
|
|
6504
6825
|
showThinking();
|
|
6505
6826
|
};
|
|
6506
6827
|
|
|
@@ -6699,7 +7020,7 @@
|
|
|
6699
7020
|
if (currentSession?.mode === 'agent') {
|
|
6700
7021
|
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
6701
7022
|
addInputMessage('🤖 让 AI 过一遍记忆(找过时/重复/废弃)');
|
|
6702
|
-
socket.emit('agent:query', { prompt });
|
|
7023
|
+
socket.emit('agent:query', { prompt, display: '🤖 让 AI 过一遍记忆(找过时/重复/废弃)' });
|
|
6703
7024
|
showThinking();
|
|
6704
7025
|
} else {
|
|
6705
7026
|
addStatusMessage('该功能仅在 agent(Claude) 会话可用');
|