codexmate 0.0.13 → 0.0.15
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.en.md +213 -0
- package/README.md +147 -346
- package/cli.js +3163 -224
- package/doc/CHANGELOG.md +13 -1
- package/doc/CHANGELOG.zh-CN.md +14 -0
- package/lib/cli-utils.js +16 -0
- package/lib/workflow-engine.js +340 -0
- package/package.json +10 -3
- package/web-ui/app.js +275 -65
- package/web-ui/index.html +279 -33
- package/web-ui/logic.mjs +147 -1
- package/web-ui/modules/config-mode.computed.mjs +123 -0
- package/web-ui/modules/skills.computed.mjs +82 -0
- package/web-ui/modules/skills.methods.mjs +344 -0
- package/web-ui/styles.css +648 -10
- package/README.zh-CN.md +0 -419
package/web-ui/index.html
CHANGED
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
<body>
|
|
14
14
|
<div id="app" class="container" v-cloak>
|
|
15
15
|
<button class="fab-install" @click="openInstallModal" aria-label="安装 CLI">
|
|
16
|
-
|
|
16
|
+
<span class="fab-install-icon" aria-hidden="true">
|
|
17
|
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
18
|
+
<path d="M10 4v8M6.5 8.5 10 12l3.5-3.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
19
|
+
<path d="M4 14.5h12" stroke-linecap="round"/>
|
|
20
|
+
</svg>
|
|
21
|
+
</span>
|
|
17
22
|
</button>
|
|
18
23
|
|
|
19
24
|
<div class="hero" v-if="!sessionStandalone">
|
|
@@ -26,6 +31,26 @@
|
|
|
26
31
|
<span class="sr-only">本地配置中枢,统一管理 Codex / Claude Code / OpenClaw / 会话。</span>
|
|
27
32
|
</div>
|
|
28
33
|
</div>
|
|
34
|
+
<div v-if="!sessionStandalone" class="hero-github">
|
|
35
|
+
<a
|
|
36
|
+
class="github-badge github-badge-mobile"
|
|
37
|
+
href="https://github.com/SakuraByteCore/codexmate"
|
|
38
|
+
target="_blank"
|
|
39
|
+
rel="noopener"
|
|
40
|
+
title="打开 GitHub 仓库">
|
|
41
|
+
<span class="github-badge-left">
|
|
42
|
+
<svg class="github-badge-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
43
|
+
<path fill="currentColor" d="M12 2C6.48 2 2 6.58 2 12.26c0 4.5 2.87 8.32 6.84 9.67.5.1.68-.22.68-.49 0-.24-.01-.88-.01-1.73-2.78.62-3.37-1.38-3.37-1.38-.45-1.19-1.11-1.5-1.11-1.5-.91-.64.07-.63.07-.63 1 .07 1.53 1.06 1.53 1.06.9 1.57 2.36 1.12 2.94.86.09-.66.35-1.12.63-1.38-2.22-.26-4.56-1.14-4.56-5.09 0-1.13.39-2.06 1.03-2.79-.1-.26-.45-1.31.1-2.73 0 0 .84-.28 2.75 1.06A9.36 9.36 0 0 1 12 6.8c.85 0 1.7.12 2.5.34 1.9-1.34 2.74-1.06 2.74-1.06.55 1.42.2 2.47.1 2.73.64.73 1.03 1.66 1.03 2.79 0 3.96-2.35 4.83-4.58 5.08.36.32.68.95.68 1.92 0 1.38-.01 2.5-.01 2.84 0 .27.18.59.69.49A10.04 10.04 0 0 0 22 12.26C22 6.58 17.52 2 12 2z"/>
|
|
44
|
+
</svg>
|
|
45
|
+
<span class="github-badge-label">GitHub</span>
|
|
46
|
+
</span>
|
|
47
|
+
<span class="github-badge-text" title="SakuraByteCore/codexmate">
|
|
48
|
+
<span class="github-owner">SakuraByteCore</span>
|
|
49
|
+
<span class="github-sep">/</span>
|
|
50
|
+
<span class="github-repo">codexmate</span>
|
|
51
|
+
</span>
|
|
52
|
+
</a>
|
|
53
|
+
</div>
|
|
29
54
|
|
|
30
55
|
<div v-if="!sessionStandalone" class="top-tabs" role="tablist" aria-label="主导航">
|
|
31
56
|
<button class="top-tab"
|
|
@@ -34,7 +59,7 @@
|
|
|
34
59
|
:tabindex="mainTab === 'config' && configMode === 'codex' ? 0 : -1"
|
|
35
60
|
:aria-selected="mainTab === 'config' && configMode === 'codex'"
|
|
36
61
|
:aria-pressed="mainTab === 'config' && configMode === 'codex'"
|
|
37
|
-
aria-controls="panel-config-
|
|
62
|
+
aria-controls="panel-config-provider"
|
|
38
63
|
:class="{ active: mainTab === 'config' && configMode === 'codex' }"
|
|
39
64
|
@click="switchConfigMode('codex')">Codex 配置</button>
|
|
40
65
|
<button class="top-tab"
|
|
@@ -87,6 +112,24 @@
|
|
|
87
112
|
<div class="brand-subtitle">
|
|
88
113
|
配置 / 会话切换器
|
|
89
114
|
</div>
|
|
115
|
+
<a
|
|
116
|
+
class="github-badge github-badge-rail"
|
|
117
|
+
href="https://github.com/SakuraByteCore/codexmate"
|
|
118
|
+
target="_blank"
|
|
119
|
+
rel="noopener"
|
|
120
|
+
title="打开 GitHub 仓库">
|
|
121
|
+
<span class="github-badge-left">
|
|
122
|
+
<svg class="github-badge-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
123
|
+
<path fill="currentColor" d="M12 2C6.48 2 2 6.58 2 12.26c0 4.5 2.87 8.32 6.84 9.67.5.1.68-.22.68-.49 0-.24-.01-.88-.01-1.73-2.78.62-3.37-1.38-3.37-1.38-.45-1.19-1.11-1.5-1.11-1.5-.91-.64.07-.63.07-.63 1 .07 1.53 1.06 1.53 1.06.9 1.57 2.36 1.12 2.94.86.09-.66.35-1.12.63-1.38-2.22-.26-4.56-1.14-4.56-5.09 0-1.13.39-2.06 1.03-2.79-.1-.26-.45-1.31.1-2.73 0 0 .84-.28 2.75 1.06A9.36 9.36 0 0 1 12 6.8c.85 0 1.7.12 2.5.34 1.9-1.34 2.74-1.06 2.74-1.06.55 1.42.2 2.47.1 2.73.64.73 1.03 1.66 1.03 2.79 0 3.96-2.35 4.83-4.58 5.08.36.32.68.95.68 1.92 0 1.38-.01 2.5-.01 2.84 0 .27.18.59.69.49A10.04 10.04 0 0 0 22 12.26C22 6.58 17.52 2 12 2z"/>
|
|
124
|
+
</svg>
|
|
125
|
+
<span class="github-badge-label">GitHub</span>
|
|
126
|
+
</span>
|
|
127
|
+
<span class="github-badge-text" title="SakuraByteCore/codexmate">
|
|
128
|
+
<span class="github-owner">SakuraByteCore</span>
|
|
129
|
+
<span class="github-sep">/</span>
|
|
130
|
+
<span class="github-repo">codexmate</span>
|
|
131
|
+
</span>
|
|
132
|
+
</a>
|
|
90
133
|
</div>
|
|
91
134
|
|
|
92
135
|
<div class="side-section" role="tablist" aria-label="配置管理">
|
|
@@ -94,7 +137,7 @@
|
|
|
94
137
|
<button
|
|
95
138
|
role="tab"
|
|
96
139
|
id="side-tab-config-codex"
|
|
97
|
-
aria-controls="panel-config-
|
|
140
|
+
aria-controls="panel-config-provider"
|
|
98
141
|
:tabindex="mainTab === 'config' && configMode === 'codex' ? 0 : -1"
|
|
99
142
|
:aria-selected="mainTab === 'config' && configMode === 'codex'"
|
|
100
143
|
:aria-pressed="mainTab === 'config' && configMode === 'codex'"
|
|
@@ -190,13 +233,13 @@
|
|
|
190
233
|
</div>
|
|
191
234
|
|
|
192
235
|
<div class="status-strip" v-if="!sessionStandalone && mainTab === 'config'">
|
|
193
|
-
<template v-if="
|
|
236
|
+
<template v-if="isProviderConfigMode">
|
|
194
237
|
<div class="status-chip">
|
|
195
|
-
<span class="label">
|
|
238
|
+
<span class="label">{{ activeProviderConfigChipLabel }}</span>
|
|
196
239
|
<span class="value">{{ currentProvider || '未选择' }}</span>
|
|
197
240
|
</div>
|
|
198
241
|
<div class="status-chip">
|
|
199
|
-
<span class="label">
|
|
242
|
+
<span class="label">{{ activeProviderModelChipLabel }}</span>
|
|
200
243
|
<span class="value">{{ currentModel || '未选择' }}</span>
|
|
201
244
|
</div>
|
|
202
245
|
</template>
|
|
@@ -210,7 +253,7 @@
|
|
|
210
253
|
<span class="value">{{ currentClaudeModel || '未选择' }}</span>
|
|
211
254
|
</div>
|
|
212
255
|
</template>
|
|
213
|
-
<template v-else>
|
|
256
|
+
<template v-else-if="configMode === 'openclaw'">
|
|
214
257
|
<div class="status-chip">
|
|
215
258
|
<span class="label">OpenClaw 配置</span>
|
|
216
259
|
<span class="value">{{ currentOpenclawConfig || '未选择' }}</span>
|
|
@@ -220,6 +263,12 @@
|
|
|
220
263
|
<span class="value">{{ openclawWorkspaceFileName || '未选择' }}</span>
|
|
221
264
|
</div>
|
|
222
265
|
</template>
|
|
266
|
+
<template v-else>
|
|
267
|
+
<div class="status-chip">
|
|
268
|
+
<span class="label">配置模式</span>
|
|
269
|
+
<span class="value">未选择</span>
|
|
270
|
+
</div>
|
|
271
|
+
</template>
|
|
223
272
|
</div>
|
|
224
273
|
<div class="status-strip" v-else-if="!sessionStandalone && mainTab === 'sessions'">
|
|
225
274
|
<div class="status-chip">
|
|
@@ -248,13 +297,13 @@
|
|
|
248
297
|
|
|
249
298
|
<!-- 内容包裹器 - 稳定布局 -->
|
|
250
299
|
<div class="content-wrapper">
|
|
251
|
-
<!-- Codex
|
|
300
|
+
<!-- Provider 配置模式(Codex) -->
|
|
252
301
|
<div
|
|
253
|
-
v-show="mainTab === 'config' &&
|
|
302
|
+
v-show="mainTab === 'config' && isProviderConfigMode"
|
|
254
303
|
class="mode-content mode-cards"
|
|
255
|
-
id="panel-config-
|
|
304
|
+
id="panel-config-provider"
|
|
256
305
|
role="tabpanel"
|
|
257
|
-
:aria-labelledby="'tab-config-
|
|
306
|
+
:aria-labelledby="'tab-config-' + configMode">
|
|
258
307
|
<!-- 添加提供商按钮 -->
|
|
259
308
|
<button class="btn-add" @click="showAddModal = true" v-if="!loading && !initError">
|
|
260
309
|
<svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
|
|
@@ -287,7 +336,7 @@
|
|
|
287
336
|
class="model-input"
|
|
288
337
|
v-model="currentModel"
|
|
289
338
|
@blur="onModelChange"
|
|
290
|
-
placeholder="
|
|
339
|
+
:placeholder="activeProviderModelPlaceholder"
|
|
291
340
|
>
|
|
292
341
|
<div class="config-template-hint" v-if="modelsSource === 'unlimited'">
|
|
293
342
|
当前提供商未提供模型列表,视为不限。模型可手动输入。
|
|
@@ -296,16 +345,20 @@
|
|
|
296
345
|
模型列表获取失败,请检查接口或手动输入。
|
|
297
346
|
</div>
|
|
298
347
|
<div class="config-template-hint" v-if="modelsSource === 'remote' && !modelsHasCurrent">
|
|
299
|
-
当前模型不在接口列表中,请手动输入或在模板中调整。
|
|
348
|
+
{{ isCodexConfigMode ? '当前模型不在接口列表中,请手动输入或在模板中调整。' : '当前模型不在接口列表中,请手动输入。' }}
|
|
300
349
|
</div>
|
|
301
|
-
<div class="config-template-hint">
|
|
350
|
+
<div class="config-template-hint" v-if="isCodexConfigMode">
|
|
302
351
|
Codex 配置需先改模板,再手动应用。
|
|
303
352
|
</div>
|
|
304
|
-
<
|
|
353
|
+
<div class="config-template-hint" v-else-if="activeProviderBridgeHint">
|
|
354
|
+
{{ activeProviderBridgeHint }} 模板、认证和代理仅在 Codex 模式下可编辑。
|
|
355
|
+
</div>
|
|
356
|
+
<button class="btn-tool btn-template-editor" v-if="isCodexConfigMode" @click="openConfigTemplateEditor" :disabled="loading || !!initError">
|
|
305
357
|
打开 Config 模板编辑器
|
|
306
358
|
</button>
|
|
307
359
|
</div>
|
|
308
360
|
|
|
361
|
+
<template v-if="isCodexConfigMode">
|
|
309
362
|
<div class="selector-section">
|
|
310
363
|
<div class="selector-header">
|
|
311
364
|
<span class="selector-title">服务档位</span>
|
|
@@ -343,6 +396,16 @@
|
|
|
343
396
|
</button>
|
|
344
397
|
</div>
|
|
345
398
|
|
|
399
|
+
<div class="selector-section">
|
|
400
|
+
<div class="selector-header">
|
|
401
|
+
<span class="selector-title">Skills 管理</span>
|
|
402
|
+
</div>
|
|
403
|
+
<div class="config-template-hint skills-hint-line">管理 <code>~/.codex/skills</code> 自定义 skills,弹窗提供统计概览、筛选检索、多选删除、ZIP 导入与导出。</div>
|
|
404
|
+
<button class="btn-tool" @click="openSkillsManager" :disabled="loading || !!initError || skillsLoading || skillsDeleting || skillsScanningImports || skillsImporting || skillsZipImporting || skillsExporting">
|
|
405
|
+
{{ skillsLoading ? '加载中...' : '打开 Skills 管理' }}
|
|
406
|
+
</button>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
346
409
|
<div class="selector-section">
|
|
347
410
|
<div class="selector-header">
|
|
348
411
|
<span class="selector-title">配置维护</span>
|
|
@@ -357,8 +420,8 @@
|
|
|
357
420
|
<div class="selector-header">
|
|
358
421
|
<span class="selector-title">Codex 认证文件</span>
|
|
359
422
|
</div>
|
|
360
|
-
<div class="config-template-hint">
|
|
361
|
-
上传 JSON
|
|
423
|
+
<div class="config-template-hint hint-single-line" title="上传 JSON 切换账号(写入 ~/.codex/auth.json)。">
|
|
424
|
+
上传 JSON 切换账号(写入 <code>~/.codex/auth.json</code>)。
|
|
362
425
|
</div>
|
|
363
426
|
<button class="btn-tool" @click="triggerCodexAuthUpload" :disabled="codexAuthImportLoading || loading || !!initError">
|
|
364
427
|
{{ codexAuthImportLoading ? '上传中...' : '上传认证文件' }}
|
|
@@ -449,6 +512,7 @@
|
|
|
449
512
|
<span v-if="proxyRuntime.upstreamProvider">(上游 <code>{{ proxyRuntime.upstreamProvider }}</code>)</span>
|
|
450
513
|
</div>
|
|
451
514
|
</div>
|
|
515
|
+
</template>
|
|
452
516
|
|
|
453
517
|
<div class="selector-section">
|
|
454
518
|
<div class="selector-header">
|
|
@@ -845,10 +909,10 @@
|
|
|
845
909
|
</div>
|
|
846
910
|
</div>
|
|
847
911
|
|
|
848
|
-
<div :class="['session-preview', { active: !!activeSession }]">
|
|
912
|
+
<div :class="['session-preview', { active: !!activeSession }]" :ref="setSessionPreviewContainerRef">
|
|
849
913
|
<template v-if="activeSession">
|
|
850
|
-
<div class="session-preview-scroll">
|
|
851
|
-
<div class="session-preview-header">
|
|
914
|
+
<div class="session-preview-scroll" :ref="setSessionPreviewScrollRef" @scroll="onSessionPreviewScroll">
|
|
915
|
+
<div class="session-preview-header" :ref="setSessionPreviewHeaderRef">
|
|
852
916
|
<div>
|
|
853
917
|
<div class="session-preview-title">{{ activeSession.title || activeSession.sessionId }}</div>
|
|
854
918
|
<div class="session-preview-meta">
|
|
@@ -899,23 +963,44 @@
|
|
|
899
963
|
</div>
|
|
900
964
|
|
|
901
965
|
<div v-else class="session-preview-body">
|
|
902
|
-
<div
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
966
|
+
<div class="session-preview-messages">
|
|
967
|
+
<div v-if="activeSessionDetailClipped" class="session-item-sub session-item-wrap">
|
|
968
|
+
仅展示最近 {{ activeSessionMessages.length }} 条消息。
|
|
969
|
+
</div>
|
|
970
|
+
<div
|
|
971
|
+
v-for="(msg, idx) in activeSessionMessages"
|
|
972
|
+
:key="getRecordRenderKey(msg, idx)"
|
|
973
|
+
:data-message-key="getRecordRenderKey(msg, idx)"
|
|
974
|
+
:ref="(el) => bindSessionMessageRef(getRecordRenderKey(msg, idx), el)"
|
|
975
|
+
:class="['session-msg', msg.normalizedRole === 'user' ? 'user' : (msg.normalizedRole === 'system' ? 'system' : 'assistant')]">
|
|
976
|
+
<div class="session-msg-header">
|
|
977
|
+
<div class="session-msg-meta">
|
|
978
|
+
<span class="session-msg-role">{{ msg.roleLabel || (msg.normalizedRole === 'user' ? 'User' : (msg.normalizedRole === 'system' ? 'System' : 'Assistant')) }}</span>
|
|
979
|
+
<span class="session-msg-time">{{ msg.timestamp || '' }}</span>
|
|
980
|
+
</div>
|
|
913
981
|
</div>
|
|
982
|
+
<div class="session-msg-content">{{ msg.text || '' }}</div>
|
|
914
983
|
</div>
|
|
915
|
-
<div class="session-msg-content">{{ msg.text || '' }}</div>
|
|
916
984
|
</div>
|
|
917
985
|
</div>
|
|
918
986
|
</div>
|
|
987
|
+
<aside v-if="sessionTimelineNodes.length" class="session-timeline" aria-label="会话时间轴">
|
|
988
|
+
<div class="session-timeline-track"></div>
|
|
989
|
+
<button
|
|
990
|
+
v-for="node in sessionTimelineNodes"
|
|
991
|
+
:key="'timeline-' + node.key"
|
|
992
|
+
type="button"
|
|
993
|
+
:class="['session-timeline-node', { active: sessionTimelineActiveKey === node.key }]"
|
|
994
|
+
:aria-current="sessionTimelineActiveKey === node.key ? 'true' : null"
|
|
995
|
+
:style="{ top: `${node.safePercent}%` }"
|
|
996
|
+
:title="node.title"
|
|
997
|
+
@click="jumpToSessionTimelineNode(node.key)">
|
|
998
|
+
<span class="sr-only">{{ node.title }}</span>
|
|
999
|
+
</button>
|
|
1000
|
+
<div class="session-timeline-current" v-if="sessionTimelineActiveTitle">
|
|
1001
|
+
{{ sessionTimelineActiveTitle }}
|
|
1002
|
+
</div>
|
|
1003
|
+
</aside>
|
|
919
1004
|
</template>
|
|
920
1005
|
|
|
921
1006
|
<div v-else class="session-preview-empty">
|
|
@@ -1593,6 +1678,168 @@
|
|
|
1593
1678
|
</div>
|
|
1594
1679
|
</div>
|
|
1595
1680
|
|
|
1681
|
+
<div v-if="showSkillsModal" class="modal-overlay" @click.self="closeSkillsModal">
|
|
1682
|
+
<div class="modal modal-wide skills-modal">
|
|
1683
|
+
<div class="modal-header skills-modal-header">
|
|
1684
|
+
<div>
|
|
1685
|
+
<div class="modal-title">Skills 管理</div>
|
|
1686
|
+
<div class="skills-modal-subtitle">集中管理本地技能目录,支持检索筛选、多选删除、跨应用导入、ZIP 导入与导出。</div>
|
|
1687
|
+
</div>
|
|
1688
|
+
<div class="modal-header-actions skills-modal-actions">
|
|
1689
|
+
<button class="btn-mini" @click="refreshSkillsList({ silent: false })" :disabled="skillsLoading || skillsDeleting || skillsScanningImports || skillsImporting || skillsZipImporting || skillsExporting">
|
|
1690
|
+
{{ skillsLoading ? '刷新中...' : '刷新' }}
|
|
1691
|
+
</button>
|
|
1692
|
+
</div>
|
|
1693
|
+
</div>
|
|
1694
|
+
|
|
1695
|
+
<div class="form-group skills-root-group">
|
|
1696
|
+
<label class="form-label">Skills 目录</label>
|
|
1697
|
+
<div class="skills-root-box">{{ skillsRootPath || '~/.codex/skills' }}</div>
|
|
1698
|
+
</div>
|
|
1699
|
+
|
|
1700
|
+
<div class="skills-summary-strip">
|
|
1701
|
+
<div class="skills-summary-item">
|
|
1702
|
+
<span class="skills-summary-label">本地总数</span>
|
|
1703
|
+
<strong class="skills-summary-value">{{ skillsList.length }}</strong>
|
|
1704
|
+
</div>
|
|
1705
|
+
<div class="skills-summary-item">
|
|
1706
|
+
<span class="skills-summary-label">含 SKILL.md</span>
|
|
1707
|
+
<strong class="skills-summary-value">{{ skillsConfiguredCount }}</strong>
|
|
1708
|
+
</div>
|
|
1709
|
+
<div class="skills-summary-item">
|
|
1710
|
+
<span class="skills-summary-label">缺少 SKILL.md</span>
|
|
1711
|
+
<strong class="skills-summary-value">{{ skillsMissingSkillFileCount }}</strong>
|
|
1712
|
+
</div>
|
|
1713
|
+
<div class="skills-summary-item">
|
|
1714
|
+
<span class="skills-summary-label">可导入</span>
|
|
1715
|
+
<strong class="skills-summary-value">{{ skillsImportList.length }}</strong>
|
|
1716
|
+
</div>
|
|
1717
|
+
</div>
|
|
1718
|
+
|
|
1719
|
+
<div class="skills-panel">
|
|
1720
|
+
<div class="skills-panel-header">
|
|
1721
|
+
<div class="skills-panel-title-wrap">
|
|
1722
|
+
<div class="skills-panel-title">本地 Skills</div>
|
|
1723
|
+
<div class="skills-panel-note">支持关键词检索与状态筛选,勾选后可批量删除。</div>
|
|
1724
|
+
</div>
|
|
1725
|
+
<button
|
|
1726
|
+
class="btn-mini"
|
|
1727
|
+
@click="resetSkillsFilters"
|
|
1728
|
+
:disabled="skillsLoading || skillsDeleting || !skillsFilterDirty">
|
|
1729
|
+
重置筛选
|
|
1730
|
+
</button>
|
|
1731
|
+
</div>
|
|
1732
|
+
|
|
1733
|
+
<div class="skills-filter-row">
|
|
1734
|
+
<input
|
|
1735
|
+
class="form-input"
|
|
1736
|
+
type="text"
|
|
1737
|
+
v-model.trim="skillsKeyword"
|
|
1738
|
+
aria-label="按名称或描述筛选 skill"
|
|
1739
|
+
placeholder="按目录名/显示名/描述检索">
|
|
1740
|
+
<select class="form-select skills-status-select" v-model="skillsStatusFilter" aria-label="按 SKILL.md 状态筛选 skill">
|
|
1741
|
+
<option value="all">全部状态</option>
|
|
1742
|
+
<option value="with-skill-file">仅含 SKILL.md</option>
|
|
1743
|
+
<option value="missing-skill-file">仅缺少 SKILL.md</option>
|
|
1744
|
+
</select>
|
|
1745
|
+
</div>
|
|
1746
|
+
|
|
1747
|
+
<div class="skill-toolbar">
|
|
1748
|
+
<label class="skill-select-all">
|
|
1749
|
+
<input type="checkbox" :checked="skillsAllSelected" @change="toggleAllSkillsSelection" :disabled="skillsLoading || skillsDeleting || skillsSelectableNames.length === 0">
|
|
1750
|
+
<span>{{ skillsAllSelected ? '取消全选' : '全选' }}</span>
|
|
1751
|
+
</label>
|
|
1752
|
+
<span class="skill-toolbar-count">已选 {{ skillsSelectedCount }}(筛选命中 {{ filteredSkillsList.length }} / {{ skillsList.length }},筛选内已选 {{ skillsVisibleSelectedCount }})</span>
|
|
1753
|
+
</div>
|
|
1754
|
+
|
|
1755
|
+
<div v-if="skillsList.length === 0" class="skills-empty-state">暂无可管理的 skill。</div>
|
|
1756
|
+
<div v-else-if="filteredSkillsList.length === 0" class="skills-empty-state">当前筛选条件下没有匹配的 skill。</div>
|
|
1757
|
+
<div v-else class="skill-list">
|
|
1758
|
+
<label
|
|
1759
|
+
class="skill-item"
|
|
1760
|
+
:class="{ selected: skillsSelectedNames.includes(skill.name) }"
|
|
1761
|
+
v-for="skill in filteredSkillsList"
|
|
1762
|
+
:key="'skill-' + skill.name">
|
|
1763
|
+
<input type="checkbox" v-model="skillsSelectedNames" :value="skill.name" :disabled="skillsDeleting">
|
|
1764
|
+
<div class="skill-item-main">
|
|
1765
|
+
<div class="skill-item-title">{{ skill.displayName || skill.name }}</div>
|
|
1766
|
+
<div v-if="skill.description" class="skill-item-description">{{ skill.description }}</div>
|
|
1767
|
+
<div class="skill-item-meta">
|
|
1768
|
+
<span class="skill-item-path" :title="skill.path">{{ skill.path }}</span>
|
|
1769
|
+
<span :class="['pill', skill.hasSkillFile ? 'configured' : 'empty']">
|
|
1770
|
+
{{ skill.hasSkillFile ? '含 SKILL.md' : '缺少 SKILL.md' }}
|
|
1771
|
+
</span>
|
|
1772
|
+
<span class="pill source">{{ skill.sourceType === 'symlink' ? '符号链接' : '目录' }}</span>
|
|
1773
|
+
</div>
|
|
1774
|
+
</div>
|
|
1775
|
+
</label>
|
|
1776
|
+
</div>
|
|
1777
|
+
</div>
|
|
1778
|
+
|
|
1779
|
+
<div class="skills-panel skills-import-block">
|
|
1780
|
+
<div class="skills-panel-header">
|
|
1781
|
+
<div class="skills-panel-title-wrap">
|
|
1782
|
+
<div class="skills-import-title">跨应用导入(对齐 cc-switch 能力)</div>
|
|
1783
|
+
<div class="skills-panel-note">从其他应用扫描并导入未托管技能,支持多选批量导入。</div>
|
|
1784
|
+
</div>
|
|
1785
|
+
<button class="btn-mini" @click="scanImportableSkills" :disabled="skillsLoading || skillsDeleting || skillsScanningImports || skillsImporting || skillsZipImporting || skillsExporting">
|
|
1786
|
+
{{ skillsScanningImports ? '扫描中...' : '扫描可导入' }}
|
|
1787
|
+
</button>
|
|
1788
|
+
</div>
|
|
1789
|
+
<div class="skill-toolbar">
|
|
1790
|
+
<label class="skill-select-all">
|
|
1791
|
+
<input type="checkbox" :checked="skillsImportAllSelected" @change="toggleAllSkillsImportSelection" :disabled="skillsScanningImports || skillsImporting || skillsImportSelectableKeys.length === 0">
|
|
1792
|
+
<span>{{ skillsImportAllSelected ? '取消全选' : '全选' }}</span>
|
|
1793
|
+
</label>
|
|
1794
|
+
<span class="skill-toolbar-count">已选 {{ skillsImportSelectedCount }} / {{ skillsImportSelectableKeys.length }},含 SKILL.md {{ skillsImportConfiguredCount }},缺失 {{ skillsImportMissingSkillFileCount }}</span>
|
|
1795
|
+
</div>
|
|
1796
|
+
<div v-if="skillsImportList.length === 0" class="skills-empty-state skills-import-empty">暂无可导入 skill,点击“扫描可导入”。</div>
|
|
1797
|
+
<div v-else class="skill-list skills-import-list">
|
|
1798
|
+
<label
|
|
1799
|
+
class="skill-item"
|
|
1800
|
+
:class="{ selected: skillsImportSelectedKeys.includes(buildSkillImportKey(skill)) }"
|
|
1801
|
+
v-for="skill in skillsImportList"
|
|
1802
|
+
:key="'import-skill-' + buildSkillImportKey(skill)">
|
|
1803
|
+
<input type="checkbox" v-model="skillsImportSelectedKeys" :value="buildSkillImportKey(skill)" :disabled="skillsImporting">
|
|
1804
|
+
<div class="skill-item-main">
|
|
1805
|
+
<div class="skill-item-title">{{ skill.displayName || skill.name }}</div>
|
|
1806
|
+
<div v-if="skill.description" class="skill-item-description">{{ skill.description }}</div>
|
|
1807
|
+
<div class="skill-item-meta">
|
|
1808
|
+
<span class="skill-item-path" :title="skill.sourcePath">{{ skill.sourcePath }}</span>
|
|
1809
|
+
<span class="pill source">{{ skill.sourceLabel }}</span>
|
|
1810
|
+
<span :class="['pill', skill.hasSkillFile ? 'configured' : 'empty']">
|
|
1811
|
+
{{ skill.hasSkillFile ? '含 SKILL.md' : '缺少 SKILL.md' }}
|
|
1812
|
+
</span>
|
|
1813
|
+
</div>
|
|
1814
|
+
</div>
|
|
1815
|
+
</label>
|
|
1816
|
+
</div>
|
|
1817
|
+
</div>
|
|
1818
|
+
|
|
1819
|
+
<div class="btn-group">
|
|
1820
|
+
<button class="btn btn-cancel" @click="closeSkillsModal" :disabled="skillsLoading || skillsDeleting || skillsImporting || skillsScanningImports || skillsZipImporting || skillsExporting">关闭</button>
|
|
1821
|
+
<button class="btn btn-cancel" @click="triggerSkillsZipImport" :disabled="skillsZipImporting || skillsDeleting || skillsImporting || skillsScanningImports || skillsExporting">
|
|
1822
|
+
{{ skillsZipImporting ? 'ZIP 导入中...' : '导入 ZIP' }}
|
|
1823
|
+
</button>
|
|
1824
|
+
<button class="btn btn-confirm" @click="exportSelectedSkills" :disabled="skillsExporting || skillsSelectedCount === 0 || skillsDeleting || skillsImporting || skillsScanningImports || skillsZipImporting">
|
|
1825
|
+
{{ skillsExporting ? '导出中...' : '导出选中' }}
|
|
1826
|
+
</button>
|
|
1827
|
+
<button class="btn btn-confirm" @click="importSelectedSkills" :disabled="skillsImporting || skillsImportSelectedCount === 0 || skillsZipImporting || skillsExporting || skillsDeleting">
|
|
1828
|
+
{{ skillsImporting ? '导入中...' : '导入选中' }}
|
|
1829
|
+
</button>
|
|
1830
|
+
<button class="btn btn-confirm btn-danger" @click="deleteSelectedSkills" :disabled="skillsDeleting || skillsSelectedCount === 0 || skillsImporting || skillsZipImporting || skillsExporting">
|
|
1831
|
+
{{ skillsDeleting ? '删除中...' : '删除选中' }}
|
|
1832
|
+
</button>
|
|
1833
|
+
</div>
|
|
1834
|
+
<input
|
|
1835
|
+
ref="skillsZipImportInput"
|
|
1836
|
+
type="file"
|
|
1837
|
+
accept=".zip,application/zip"
|
|
1838
|
+
style="display:none"
|
|
1839
|
+
@change="handleSkillsZipImportChange">
|
|
1840
|
+
</div>
|
|
1841
|
+
</div>
|
|
1842
|
+
|
|
1596
1843
|
<!-- Toast通知 -->
|
|
1597
1844
|
|
|
1598
1845
|
<!-- Toast -->
|
|
@@ -1602,4 +1849,3 @@
|
|
|
1602
1849
|
<script type="module" src="web-ui/app.js"></script>
|
|
1603
1850
|
</body>
|
|
1604
1851
|
</html>
|
|
1605
|
-
|
package/web-ui/logic.mjs
CHANGED
|
@@ -128,10 +128,31 @@ export function buildSpeedTestIssue(name, result) {
|
|
|
128
128
|
|
|
129
129
|
// Session filtering helpers
|
|
130
130
|
export function isSessionQueryEnabled(source) {
|
|
131
|
-
const normalized = (source
|
|
131
|
+
const normalized = normalizeSessionSource(source, '');
|
|
132
132
|
return normalized === 'codex' || normalized === 'claude' || normalized === 'all';
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
export function normalizeSessionSource(source, fallback = 'all') {
|
|
136
|
+
const normalized = typeof source === 'string'
|
|
137
|
+
? source.trim().toLowerCase()
|
|
138
|
+
: '';
|
|
139
|
+
if (normalized === 'codex' || normalized === 'claude' || normalized === 'all') {
|
|
140
|
+
return normalized;
|
|
141
|
+
}
|
|
142
|
+
return fallback;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function normalizeSessionPathFilter(pathFilter) {
|
|
146
|
+
return typeof pathFilter === 'string' ? pathFilter.trim() : '';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildSessionFilterCacheState(source, pathFilter) {
|
|
150
|
+
return {
|
|
151
|
+
source: normalizeSessionSource(source, 'all'),
|
|
152
|
+
pathFilter: normalizeSessionPathFilter(pathFilter)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
135
156
|
export function buildSessionListParams(options = {}) {
|
|
136
157
|
const {
|
|
137
158
|
source = 'all',
|
|
@@ -155,3 +176,128 @@ export function buildSessionListParams(options = {}) {
|
|
|
155
176
|
forceRefresh: true
|
|
156
177
|
};
|
|
157
178
|
}
|
|
179
|
+
|
|
180
|
+
export function normalizeSessionMessageRole(role) {
|
|
181
|
+
const value = typeof role === 'string' ? role.trim().toLowerCase() : '';
|
|
182
|
+
if (value === 'user' || value === 'assistant' || value === 'system') {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
return 'assistant';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toRoleMeta(role) {
|
|
189
|
+
if (role === 'user') {
|
|
190
|
+
return { role: 'user', roleLabel: 'User', roleShort: 'U' };
|
|
191
|
+
}
|
|
192
|
+
if (role === 'assistant') {
|
|
193
|
+
return { role: 'assistant', roleLabel: 'Assistant', roleShort: 'A' };
|
|
194
|
+
}
|
|
195
|
+
if (role === 'system') {
|
|
196
|
+
return { role: 'system', roleLabel: 'System', roleShort: 'S' };
|
|
197
|
+
}
|
|
198
|
+
return { role: 'mixed', roleLabel: 'Mixed', roleShort: 'M' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function clampTimelinePercent(percent) {
|
|
202
|
+
return Math.max(6, Math.min(94, percent));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function formatSessionTimelineTimestamp(timestamp) {
|
|
206
|
+
const value = typeof timestamp === 'string' ? timestamp.trim() : '';
|
|
207
|
+
if (!value) return '';
|
|
208
|
+
|
|
209
|
+
// 优先按 ISO/常见时间串抽取,避免本地时区格式差异导致的展示抖动。
|
|
210
|
+
const matched = value.match(/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2}))?/);
|
|
211
|
+
if (matched) {
|
|
212
|
+
const second = matched[6] || '00';
|
|
213
|
+
return `${matched[2]}-${matched[3]} ${matched[4]}:${matched[5]}:${second}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function buildSessionTimelineNodes(messages = [], options = {}) {
|
|
220
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
221
|
+
const getKey = typeof options.getKey === 'function'
|
|
222
|
+
? options.getKey
|
|
223
|
+
: ((_message, index) => `msg-${index}`);
|
|
224
|
+
const total = list.length;
|
|
225
|
+
const rawMaxMarkers = Number(options.maxMarkers);
|
|
226
|
+
const maxMarkers = Number.isFinite(rawMaxMarkers)
|
|
227
|
+
? Math.max(1, Math.min(80, Math.floor(rawMaxMarkers)))
|
|
228
|
+
: 30;
|
|
229
|
+
|
|
230
|
+
const buildSingleNode = (message, index) => {
|
|
231
|
+
const role = normalizeSessionMessageRole(message && (message.normalizedRole || message.role));
|
|
232
|
+
const roleMeta = toRoleMeta(role);
|
|
233
|
+
const key = String(getKey(message, index) || `msg-${index}`);
|
|
234
|
+
const displayTime = formatSessionTimelineTimestamp(message && message.timestamp ? message.timestamp : '');
|
|
235
|
+
const title = displayTime
|
|
236
|
+
? `#${index + 1} · ${roleMeta.roleLabel} · ${displayTime}`
|
|
237
|
+
: `#${index + 1} · ${roleMeta.roleLabel}`;
|
|
238
|
+
const percent = total <= 1 ? 0 : (index / (total - 1)) * 100;
|
|
239
|
+
return {
|
|
240
|
+
key,
|
|
241
|
+
role: roleMeta.role,
|
|
242
|
+
roleLabel: roleMeta.roleLabel,
|
|
243
|
+
roleShort: roleMeta.roleShort,
|
|
244
|
+
displayTime,
|
|
245
|
+
title,
|
|
246
|
+
percent,
|
|
247
|
+
safePercent: clampTimelinePercent(percent)
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (total <= maxMarkers) {
|
|
252
|
+
return list.map((message, index) => buildSingleNode(message, index));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const nodes = [];
|
|
256
|
+
const bucketWidth = total / maxMarkers;
|
|
257
|
+
for (let bucket = 0; bucket < maxMarkers; bucket += 1) {
|
|
258
|
+
let start = Math.floor(bucket * bucketWidth);
|
|
259
|
+
if (nodes.length && start <= nodes[nodes.length - 1].endIndex) {
|
|
260
|
+
start = nodes[nodes.length - 1].endIndex + 1;
|
|
261
|
+
}
|
|
262
|
+
if (start >= total) {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
let end = Math.floor((bucket + 1) * bucketWidth) - 1;
|
|
266
|
+
end = Math.max(start, Math.min(total - 1, end));
|
|
267
|
+
const targetIndex = Math.min(total - 1, start + Math.floor((end - start) / 2));
|
|
268
|
+
const targetMessage = list[targetIndex] || null;
|
|
269
|
+
const key = String(getKey(targetMessage, targetIndex) || `msg-${targetIndex}`);
|
|
270
|
+
const percent = total <= 1 ? 0 : (targetIndex / (total - 1)) * 100;
|
|
271
|
+
const messagesInGroup = end - start + 1;
|
|
272
|
+
const roleSet = new Set();
|
|
273
|
+
for (let i = start; i <= end; i += 1) {
|
|
274
|
+
roleSet.add(normalizeSessionMessageRole(list[i] && (list[i].normalizedRole || list[i].role)));
|
|
275
|
+
}
|
|
276
|
+
const roleValue = roleSet.size === 1 ? Array.from(roleSet)[0] : 'mixed';
|
|
277
|
+
const roleMeta = toRoleMeta(roleValue);
|
|
278
|
+
const firstTime = formatSessionTimelineTimestamp(list[start] && list[start].timestamp ? list[start].timestamp : '');
|
|
279
|
+
const lastTime = formatSessionTimelineTimestamp(list[end] && list[end].timestamp ? list[end].timestamp : '');
|
|
280
|
+
let displayTime = '';
|
|
281
|
+
if (firstTime && lastTime) {
|
|
282
|
+
displayTime = firstTime === lastTime ? firstTime : `${firstTime} ~ ${lastTime}`;
|
|
283
|
+
} else {
|
|
284
|
+
displayTime = firstTime || lastTime;
|
|
285
|
+
}
|
|
286
|
+
const titleBase = `#${start + 1}-${end + 1} · ${messagesInGroup} msgs · ${roleMeta.roleLabel}`;
|
|
287
|
+
const title = displayTime ? `${titleBase} · ${displayTime}` : titleBase;
|
|
288
|
+
nodes.push({
|
|
289
|
+
key,
|
|
290
|
+
role: roleMeta.role,
|
|
291
|
+
roleLabel: roleMeta.roleLabel,
|
|
292
|
+
roleShort: roleMeta.roleShort,
|
|
293
|
+
displayTime,
|
|
294
|
+
title,
|
|
295
|
+
percent,
|
|
296
|
+
safePercent: clampTimelinePercent(percent),
|
|
297
|
+
startIndex: start,
|
|
298
|
+
endIndex: end,
|
|
299
|
+
messageCount: messagesInGroup
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return nodes;
|
|
303
|
+
}
|