session-viewer-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -0
- package/frontend/dist-ui/index.mjs +10221 -0
- package/frontend/dist-ui/style.css +1 -0
- package/package.json +58 -0
- package/server/dist/api/cloudApi.js +267 -0
- package/server/dist/api/configApi.js +38 -0
- package/server/dist/api/sessionApi.js +151 -0
- package/server/dist/app.js +19 -0
- package/server/dist/bin.js +15 -0
- package/server/dist/index.js +2 -0
- package/server/dist/services/agcCredentials.js +117 -0
- package/server/dist/services/assembler.js +171 -0
- package/server/dist/services/cloudError.js +15 -0
- package/server/dist/services/cloudStorage.js +305 -0
- package/server/dist/services/config.js +47 -0
- package/server/dist/types/cloud.js +1 -0
- package/server/dist/types/index.js +2 -0
- package/server/dist/types/session.js +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.dir-manager[data-v-1a66345f]{background:#fff;border-radius:12px;box-shadow:0 1px 3px #00000014;margin-bottom:24px}.dir-header[data-v-1a66345f]{display:flex;align-items:center;gap:12px;width:100%;padding:16px;border:none;background:transparent;cursor:pointer;font-size:14px;text-align:left;transition:background .15s}.dir-header[data-v-1a66345f]:hover{background:#fafafa}.dir-icon[data-v-1a66345f]{width:18px;height:18px;color:#fa8c16}.dir-title[data-v-1a66345f]{font-weight:500;color:#333}.dir-toggle[data-v-1a66345f]{margin-left:auto;color:#999;width:16px;height:16px}.dir-body[data-v-1a66345f]{padding:0 16px 16px}.dir-item[data-v-1a66345f]{display:flex;align-items:center;gap:8px;padding:6px 0;font-size:13px}.dir-item-icon[data-v-1a66345f]{width:14px;height:14px;flex-shrink:0;color:#8c8c8c}.dir-item-path[data-v-1a66345f]{font-family:monospace;font-size:12px;color:#595959;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dir-badge[data-v-1a66345f]{flex-shrink:0;background:#f0f0f0;padding:1px 6px;border-radius:3px;font-size:11px;color:#8c8c8c}.dir-remove[data-v-1a66345f]{flex-shrink:0;background:none;border:none;color:#bfbfbf;cursor:pointer;padding:2px;transition:color .15s;display:flex;align-items:center}.dir-remove[data-v-1a66345f]:hover{color:#ff4d4f}.dir-remove-icon[data-v-1a66345f]{width:14px;height:14px}.dir-add[data-v-1a66345f]{display:flex;align-items:center;gap:8px;padding-top:12px}.dir-input[data-v-1a66345f]{flex:1;padding:6px 12px;border:1px solid #d9d9d9;border-radius:6px;font-family:monospace;font-size:13px;outline:none;transition:border-color .15s}.dir-input[data-v-1a66345f]:focus{border-color:#1890ff}.dir-add-btn[data-v-1a66345f]{padding:6px 16px;border:none;border-radius:6px;background:#1890ff;color:#fff;font-size:13px;cursor:pointer;white-space:nowrap;transition:background .15s}.dir-add-btn[data-v-1a66345f]:hover:not(:disabled){background:#40a9ff}.dir-add-btn[data-v-1a66345f]:disabled{opacity:.4;cursor:not-allowed}.session-card[data-v-c4930a9a]{background:#fff;border-radius:12px;padding:20px;box-shadow:0 1px 3px #00000014;cursor:pointer;transition:all .2s;position:relative;min-width:0}.session-card[data-v-c4930a9a]:hover{box-shadow:0 4px 12px #0000001f;transform:translateY(-1px)}.delete-btn[data-v-c4930a9a]{position:absolute;top:5px;right:5px;background:none;border:none;cursor:pointer;padding:4px;border-radius:4px;color:#bfbfbf;opacity:0;transition:all .15s}.session-card:hover .delete-btn[data-v-c4930a9a]{opacity:1}.delete-btn[data-v-c4930a9a]:hover{color:#ff4d4f;background:#fff1f0}.delete-btn.deleting[data-v-c4930a9a]{opacity:1;color:#1890ff;cursor:not-allowed}@keyframes spin-c4930a9a{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.spin[data-v-c4930a9a]{animation:spin-c4930a9a 1s linear infinite}.delete-icon[data-v-c4930a9a]{width:16px;height:16px}.card-header[data-v-c4930a9a]{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:12px}.card-agent[data-v-c4930a9a]{display:flex;align-items:center;gap:8px}.agent-icon[data-v-c4930a9a]{width:16px;height:16px;color:#7c3aed}.agent-name[data-v-c4930a9a]{font-size:14px;font-weight:500;color:#333}.card-model[data-v-c4930a9a]{background:#f0f0f0;padding:2px 8px;border-radius:4px;font-size:12px;color:#595959;margin-right:12px}.card-id[data-v-c4930a9a]{font-family:monospace;font-size:12px;color:#8c8c8c;margin-bottom:8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card-message[data-v-c4930a9a]{font-size:13px;color:#595959;margin-bottom:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card-dir[data-v-c4930a9a]{font-family:monospace;font-size:11px;color:#bfbfbf;margin-bottom:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card-stats[data-v-c4930a9a]{display:grid;grid-template-columns:1fr 1fr;gap:10px}.stat-item[data-v-c4930a9a]{display:flex;align-items:center;gap:6px;font-size:13px;color:#595959}.stat-icon[data-v-c4930a9a]{width:14px;height:14px;color:#8c8c8c}.card-token-bar[data-v-c4930a9a]{margin-top:12px;padding-top:12px;border-top:1px solid #f0f0f0}.token-bar-track[data-v-c4930a9a]{display:flex;height:6px;border-radius:3px;overflow:hidden;background:#f0f0f0}.token-bar-segment[data-v-c4930a9a]{height:100%;transition:width .3s}.token-bar-legend[data-v-c4930a9a]{display:flex;align-items:center;gap:12px;margin-top:6px}.legend-item[data-v-c4930a9a]{display:flex;align-items:center;gap:4px;font-size:10px;color:#8c8c8c}.legend-dot[data-v-c4930a9a]{display:inline-block;width:6px;height:6px;border-radius:2px}.legend-date[data-v-c4930a9a]{margin-left:auto;font-size:10px;color:#bfbfbf}.card-footer[data-v-c4930a9a]{margin-top:12px;padding-top:12px;border-top:1px solid #f0f0f0;display:flex;justify-content:space-between}.card-date[data-v-c4930a9a]{font-size:12px;color:#bfbfbf}.home-sidebar[data-v-e341549f]{width:100%;flex-shrink:0;border-right:1px solid #f0f0f0;background:#fff;display:flex;flex-direction:column;overflow:hidden}.sidebar-overview[data-v-e341549f]{flex-shrink:0;padding:12px 16px;border:none;background:transparent;text-align:left;font-size:14px;cursor:pointer;color:#595959;transition:all .15s;display:flex;align-items:center;gap:8px}.sidebar-overview[data-v-e341549f]:hover{background:#fafafa}.sidebar-overview.active[data-v-e341549f]{background:#1890ff14;color:#1890ff;font-weight:500}.sidebar-overview-icon[data-v-e341549f]{width:16px;height:16px;color:#d9d9d9}.sidebar-overview-icon.star-filled[data-v-e341549f]{color:#faad14}.sidebar-title[data-v-e341549f]{flex-shrink:0;padding:12px 16px;border-bottom:1px solid #f0f0f0;border-top:1px solid #f0f0f0;font-size:14px;font-weight:600;color:#333}.sidebar-sessions[data-v-e341549f]{flex:1;overflow-y:auto;padding:8px 0}.month-section[data-v-e341549f]{margin-bottom:4px}.month-header[data-v-e341549f]{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;font-size:13px;font-weight:500;color:#333;cursor:pointer;text-align:left;transition:background .15s}.month-header[data-v-e341549f]:hover{background:#fafafa}.month-label[data-v-e341549f]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.month-count[data-v-e341549f]{font-size:12px;color:#d9d9d9}.month-sessions[data-v-e341549f]{padding:2px 0}.session-item[data-v-e341549f]{display:flex;align-items:center;gap:8px;width:100%;padding:5px 16px 5px 40px;cursor:pointer;transition:all .15s}.session-item[data-v-e341549f]:hover{background:#fafafa}.session-text[data-v-e341549f]{flex:1;font-size:12px;color:#595959;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:20px}.star-btn[data-v-e341549f]{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;cursor:pointer;border-radius:4px;color:#d9d9d9;transition:all .15s}.star-btn[data-v-e341549f]:hover{background:#f0f0f0;color:#faad14}.star-btn.star-active[data-v-e341549f]{color:#faad14}.star-icon[data-v-e341549f]{width:14px;height:14px}.connection-banner[data-v-c38554e6]{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid #f3d19e;border-radius:6px;background:#fdf6ec;color:#b88230;flex-shrink:0}.banner-icon[data-v-c38554e6]{font-size:18px;line-height:1;flex-shrink:0}.banner-body[data-v-c38554e6]{flex:1;min-width:0}.banner-title[data-v-c38554e6]{font-size:14px;font-weight:600;margin:0}.banner-detail[data-v-c38554e6]{font-size:12px;margin:4px 0 0;color:#a4752a;word-break:break-all}.banner-retry[data-v-c38554e6]{flex-shrink:0;border:1px solid #d9b374;background:transparent;color:#b88230;border-radius:4px;padding:5px 14px;font-size:13px;cursor:pointer}.banner-retry[data-v-c38554e6]:hover{background:#f7e6c6}.session-viewer[data-v-fd683301]{display:flex;flex-direction:column;height:100vh}.viewer-header[data-v-fd683301]{flex-shrink:0;background:#fff;border-bottom:1px solid #f0f0f0;padding:12px 24px;display:flex;align-items:center;gap:16px;position:sticky;top:0;z-index:10}.sidebar-toggle[data-v-fd683301]{display:flex;align-items:center;padding:4px;border:none;border-radius:6px;background:transparent;cursor:pointer;color:#8c8c8c;transition:all .15s}.sidebar-toggle[data-v-fd683301]:hover{background:#f0f0f0;color:#333}.toggle-icon[data-v-fd683301]{width:18px;height:18px}.header-title[data-v-fd683301]{display:flex;align-items:center;gap:12px}.header-icon[data-v-fd683301]{font-size:20px}.header-title h1[data-v-fd683301]{font-size:18px;font-weight:600;color:#333;margin:0}.header-sub[data-v-fd683301]{font-size:13px;color:#8c8c8c;margin:0}.header-spacer[data-v-fd683301]{flex:1}.header-search[data-v-fd683301]{display:flex;align-items:center;flex-shrink:0}.search-input-wrap[data-v-fd683301]{position:relative;display:flex;align-items:center}.header-search-input[data-v-fd683301]{width:260px;padding:7px 30px 7px 32px;border:1px solid #e8e8e8;border-radius:8px;font-size:13px;outline:none;transition:border-color .15s}.header-search-input[data-v-fd683301]:focus{border-color:#1890ff}.search-icon-inner[data-v-fd683301]{position:absolute;left:10px;width:15px;height:15px;color:#bfbfbf;pointer-events:none}.search-clear[data-v-fd683301]{position:absolute;right:8px;background:none;border:none;cursor:pointer;padding:2px;display:flex;align-items:center}.clear-icon[data-v-fd683301]{width:13px;height:13px;color:#bfbfbf}.header-action-btn[data-v-fd683301]{display:flex;align-items:center;padding:4px;border:none;border-radius:6px;background:transparent;cursor:pointer;color:#8c8c8c;transition:all .15s}.header-action-btn[data-v-fd683301]:hover{background:#f0f0f0;color:#333}.action-icon[data-v-fd683301]{width:18px;height:18px}.viewer-body[data-v-fd683301]{flex:1;display:flex;overflow:hidden}.viewer-body.sidebar-collapsed .viewer-content[data-v-fd683301]{margin-left:0}.sidebar-resize-handle[data-v-fd683301]{width:4px;flex-shrink:0;cursor:col-resize;background:transparent;transition:background .15s;margin-left:-2px;margin-right:-2px;position:relative;z-index:5}.sidebar-resize-handle[data-v-fd683301]:hover,.sidebar-resize-handle[data-v-fd683301]:active{background:#1890ff}.viewer-content[data-v-fd683301]{flex:1;overflow-y:auto;padding:24px}.card-grid[data-v-fd683301]{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}@media(max-width:1024px){.card-grid[data-v-fd683301]{grid-template-columns:repeat(2,1fr)}}@media(max-width:640px){.card-grid[data-v-fd683301]{grid-template-columns:1fr}}.skeleton-card[data-v-fd683301]{background:#fff;border-radius:12px;padding:20px;box-shadow:0 1px 3px #00000014}.skeleton[data-v-fd683301]{background:linear-gradient(90deg,#f0f0f0 25%,#e8e8e8,#f0f0f0 75%);background-size:200% 100%;animation:shimmer-fd683301 1.5s infinite;border-radius:4px}.skeleton-header[data-v-fd683301]{height:20px;width:60%;margin-bottom:12px}.skeleton-id[data-v-fd683301]{height:14px;width:80%;margin-bottom:12px}.skeleton-stats[data-v-fd683301]{height:16px;width:100%}@keyframes shimmer-fd683301{0%{background-position:200% 0}to{background-position:-200% 0}}.empty-state[data-v-fd683301]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80px 0;color:#8c8c8c}.empty-icon[data-v-fd683301]{font-size:48px;margin-bottom:16px;opacity:.3}.empty-title[data-v-fd683301]{font-size:16px;font-weight:500;color:#595959;margin:0}.empty-sub[data-v-fd683301]{font-size:13px;color:#bfbfbf;margin:4px 0 0}.status-badge[data-v-4097ac6f]{display:inline-flex;align-items:center;border-radius:9999px;padding:2px 8px;font-size:12px;font-weight:500}.status-green[data-v-4097ac6f]{background:#f6ffed;color:#52c41a}.status-red[data-v-4097ac6f]{background:#fff1f0;color:#ff4d4f}.status-yellow[data-v-4097ac6f]{background:#fff7e6;color:#fa8c16}.status-blue[data-v-4097ac6f]{background:#e6f7ff;color:#1890ff}.status-gray[data-v-4097ac6f]{background:#f5f5f5;color:#8c8c8c}.step-card[data-v-aef727b1]{background:#fff;border-radius:12px;box-shadow:0 1px 3px #00000014;overflow:hidden;border-left:3px solid transparent;transition:border-color .2s,box-shadow .2s}.step-card.focused[data-v-aef727b1]{border-left-color:#1890ff;box-shadow:0 2px 8px #1890ff26}.step-detail-header[data-v-aef727b1]{display:flex;align-items:center;gap:12px;background:#fafafa;padding:12px 16px;flex-wrap:wrap}.step-detail-num[data-v-aef727b1]{width:24px;height:24px;border-radius:50%;background:#e6f7ff;color:#1890ff;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}.step-detail-title[data-v-aef727b1]{font-size:14px;font-weight:500;color:#333}.step-detail-dur[data-v-aef727b1],.step-detail-tokens[data-v-aef727b1],.step-detail-tools[data-v-aef727b1]{font-size:12px;color:#bfbfbf;font-family:monospace}.step-detail-time[data-v-aef727b1]{margin-left:auto;font-size:12px;color:#8c8c8c}.meta-icon[data-v-aef727b1]{width:12px;height:12px;vertical-align:-2px;margin-right:2px}.step-detail-body[data-v-aef727b1]{padding:16px}.section-label[data-v-aef727b1]{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600;text-transform:uppercase;color:#8c8c8c;margin:0 0 8px;cursor:pointer;-webkit-user-select:none;user-select:none}.section-label[data-v-aef727b1]:hover{color:#595959}.reasoning-label[data-v-aef727b1]{color:#722ed1}.reasoning-label[data-v-aef727b1]:hover{color:#531dab}.label-toggle[data-v-aef727b1]{font-size:10px;width:12px}.label-meta[data-v-aef727b1]{font-weight:400;font-size:11px;color:#bfbfbf;text-transform:none}.step-text[data-v-aef727b1],.step-reasoning[data-v-aef727b1]{background:#fafafa;border-radius:8px;padding:12px;font-family:SF Mono,Menlo,Monaco,Courier New,monospace;font-size:13px;line-height:1.6;color:#333;white-space:pre-wrap;word-break:break-word;max-height:384px;overflow-y:auto;margin:0 0 16px}.step-reasoning[data-v-aef727b1]{background:#f9f0ff;color:#531dab}.tool-call-card[data-v-aef727b1]{background:#fafafa;border-radius:8px;padding:12px;margin-bottom:8px}.tool-call-header[data-v-aef727b1]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.tool-call-name[data-v-aef727b1]{font-size:14px;font-weight:500;color:#333}.tool-call-dur[data-v-aef727b1]{font-size:12px;color:#8c8c8c}.tool-toggle[data-v-aef727b1]{margin-left:auto;font-size:12px;color:#1890ff;background:none;border:none;cursor:pointer}.tool-toggle[data-v-aef727b1]:hover{text-decoration:underline}.tool-call-input[data-v-aef727b1],.tool-call-output[data-v-aef727b1]{margin-top:8px}.tool-call-input pre[data-v-aef727b1],.tool-call-output pre[data-v-aef727b1]{background:#fff;border:1px solid #e8e8e8;border-radius:6px;padding:12px;font-family:SF Mono,Menlo,Monaco,Courier New,monospace;font-size:12px;line-height:1.5;color:#333;white-space:pre-wrap;word-break:break-word;max-height:300px;overflow-y:auto;margin:0}.step-detail-footer[data-v-aef727b1]{display:flex;gap:24px;padding-top:12px;margin-top:4px;border-top:1px solid #f0f0f0;font-size:12px;color:#8c8c8c}.filter-bar[data-v-adac3c66]{background:#fff;border-radius:10px;padding:10px 14px;box-shadow:0 1px 3px #00000014}.filter-top-row[data-v-adac3c66]{display:flex;align-items:center;gap:10px}.search-input-wrap[data-v-adac3c66]{width:240px;flex-shrink:0;position:relative;display:flex;align-items:center}.search-icon[data-v-adac3c66]{position:absolute;left:10px;width:15px;height:15px;color:#bfbfbf;pointer-events:none}.search-input[data-v-adac3c66]{width:100%;padding:7px 30px 7px 32px;border:1px solid #e8e8e8;border-radius:8px;font-size:13px;outline:none;transition:border-color .15s}.search-input[data-v-adac3c66]:focus{border-color:#1890ff}.search-clear[data-v-adac3c66]{position:absolute;right:8px;background:none;border:none;cursor:pointer;padding:2px;display:flex;align-items:center}.clear-icon[data-v-adac3c66]{width:13px;height:13px;color:#bfbfbf}.tool-chips-inline[data-v-adac3c66]{display:flex;align-items:center;gap:6px;flex:1;min-width:0;flex-wrap:wrap}.tool-chip[data-v-adac3c66]{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:#f0f5ff;border:1px solid #d6e4ff;border-radius:6px;font-size:12px;color:#1890ff;white-space:nowrap}.chip-remove[data-v-adac3c66]{background:none;border:none;cursor:pointer;padding:0;display:flex;align-items:center}.chip-x[data-v-adac3c66]{width:11px;height:11px;color:#1890ff}.tool-dropdown-wrap[data-v-adac3c66]{position:relative}.add-tool-btn[data-v-adac3c66]{padding:4px 10px;border:1px dashed #d9d9d9;border-radius:6px;background:#fff;font-size:12px;color:#8c8c8c;cursor:pointer;transition:all .15s;white-space:nowrap}.add-tool-btn[data-v-adac3c66]:hover{border-color:#1890ff;color:#1890ff}.tool-dropdown[data-v-adac3c66]{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e8e8e8;border-radius:8px;box-shadow:0 4px 12px #0000001a;z-index:10;min-width:140px;max-height:200px;overflow-y:auto}.tool-dropdown-item[data-v-adac3c66]{display:block;width:100%;padding:6px 12px;border:none;background:transparent;font-size:12px;color:#333;cursor:pointer;text-align:left;transition:background .15s}.tool-dropdown-item[data-v-adac3c66]:hover{background:#f0f5ff}.tool-dropdown-empty[data-v-adac3c66]{padding:6px 12px;font-size:12px;color:#bfbfbf}.filter-actions[data-v-adac3c66]{display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:auto}.filter-toggle[data-v-adac3c66]{display:flex;align-items:center;gap:5px;padding:5px 10px;border:1px solid #e8e8e8;border-radius:8px;background:#fff;font-size:12px;color:#595959;cursor:pointer;transition:all .15s}.filter-toggle[data-v-adac3c66]:hover{border-color:#1890ff;color:#1890ff}.filter-toggle.active[data-v-adac3c66]{background:#f0f5ff;border-color:#1890ff;color:#1890ff}.toggle-icon[data-v-adac3c66]{width:13px;height:13px}.filter-badge[data-v-adac3c66]{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:#1890ff;color:#fff;font-size:9px;font-weight:600}.reset-btn[data-v-adac3c66]{padding:5px 10px;border:1px solid #ff4d4f;border-radius:8px;background:#fff;font-size:12px;color:#ff4d4f;cursor:pointer;transition:all .15s}.reset-btn[data-v-adac3c66]:hover{background:#fff1f0}.filter-count[data-v-adac3c66]{font-size:12px;color:#bfbfbf;white-space:nowrap}.filter-options[data-v-adac3c66]{margin-top:10px;padding-top:10px;border-top:1px solid #f0f0f0;display:flex;gap:20px;flex-wrap:wrap}.filter-row[data-v-adac3c66]{display:flex;align-items:center;gap:6px}.filter-label[data-v-adac3c66]{font-size:12px;font-weight:500;color:#595959;white-space:nowrap}.range-input[data-v-adac3c66]{width:80px;padding:4px 8px;border:1px solid #e8e8e8;border-radius:6px;font-size:12px;outline:none;transition:border-color .15s}.range-input[data-v-adac3c66]:focus{border-color:#1890ff}.range-sep[data-v-adac3c66]{color:#d9d9d9;font-size:12px}.range-unit[data-v-adac3c66]{font-size:12px;color:#8c8c8c}.has-tools-group[data-v-adac3c66]{display:flex;gap:0;border:1px solid #e8e8e8;border-radius:6px;overflow:hidden}.has-tools-btn[data-v-adac3c66]{padding:4px 12px;border:none;background:#fff;font-size:12px;color:#595959;cursor:pointer;transition:all .15s}.has-tools-btn[data-v-adac3c66]:not(:last-child){border-right:1px solid #e8e8e8}.has-tools-btn.active[data-v-adac3c66]{background:#1890ff;color:#fff}.has-tools-btn[data-v-adac3c66]:hover:not(.active){background:#fafafa}.chart-section[data-v-2979710d]{background:#fff;border-radius:12px;padding:20px;box-shadow:0 1px 3px #00000014}.section-title[data-v-2979710d]{font-size:18px;font-weight:600;color:#333;margin:0 0 16px}.chart-container[data-v-2979710d]{position:relative}.gantt-legend[data-v-2979710d]{display:flex;align-items:center;gap:16px;margin-top:12px;padding-top:12px;border-top:1px solid #f0f0f0}.legend-item[data-v-2979710d]{display:flex;align-items:center;gap:6px;font-size:12px;color:#8c8c8c}.legend-dot[data-v-2979710d]{display:inline-block;width:10px;height:10px;border-radius:2px}.chart-section[data-v-7a01cfe3]{background:#fff;border:1px solid #e8e8e8;border-radius:8px;padding:20px;margin-bottom:16px}.section-title[data-v-7a01cfe3]{font-size:16px;font-weight:600;color:#333;margin-bottom:16px}.chart-container[data-v-7a01cfe3]{position:relative}.chart-section[data-v-dc55b693]{background:#fff;border:1px solid #e8e8e8;border-radius:8px;padding:20px;margin-bottom:16px}.section-title[data-v-dc55b693]{font-size:16px;font-weight:600;color:#333;margin-bottom:16px}.chart-container[data-v-dc55b693]{height:300px;position:relative}.chart-section[data-v-3aa0ed4e]{background:#fff;border:1px solid #e8e8e8;border-radius:8px;padding:20px;margin-bottom:16px}.section-title[data-v-3aa0ed4e]{font-size:16px;font-weight:600;color:#333;margin-bottom:16px}.chart-container[data-v-3aa0ed4e]{position:relative}.session-detail[data-v-6235eb94]{display:flex;flex-direction:column;height:100vh}.detail-header[data-v-6235eb94]{flex-shrink:0;background:#fff;border-bottom:1px solid #f0f0f0;padding:12px 24px;display:flex;align-items:center;gap:16px;position:sticky;top:0;z-index:10}.sidebar-toggle[data-v-6235eb94]{display:flex;align-items:center;padding:4px;border:none;border-radius:6px;background:transparent;cursor:pointer;color:#8c8c8c;transition:all .15s}.sidebar-toggle[data-v-6235eb94]:hover{background:#f0f0f0;color:#333}.toggle-icon[data-v-6235eb94]{width:18px;height:18px}.back-btn[data-v-6235eb94]{display:flex;align-items:center;padding:4px;border:none;border-radius:6px;background:transparent;cursor:pointer;color:#8c8c8c;transition:all .15s}.back-btn[data-v-6235eb94]:hover{background:#f0f0f0;color:#333}.back-icon[data-v-6235eb94]{width:20px;height:20px}.header-info[data-v-6235eb94]{display:flex;align-items:center;gap:12px}.header-spacer[data-v-6235eb94]{flex:1}.header-action-btn[data-v-6235eb94]{display:flex;align-items:center;padding:4px;border:none;border-radius:6px;background:transparent;cursor:pointer;color:#8c8c8c;transition:all .15s}.header-action-btn[data-v-6235eb94]:hover{background:#f0f0f0;color:#333}.action-icon[data-v-6235eb94]{width:18px;height:18px}.header-icon[data-v-6235eb94]{width:20px;height:20px;color:#7c3aed}.header-info h1[data-v-6235eb94]{font-size:18px;font-weight:600;color:#333;margin:0}.header-id[data-v-6235eb94]{font-family:monospace;font-size:12px;color:#8c8c8c;margin:0}.detail-loading[data-v-6235eb94]{flex:1;padding:24px;display:flex;flex-direction:column;gap:16px}.skeleton[data-v-6235eb94]{background:linear-gradient(90deg,#f0f0f0 25%,#e8e8e8,#f0f0f0 75%);background-size:200% 100%;animation:shimmer-6235eb94 1.5s infinite;border-radius:8px}@keyframes shimmer-6235eb94{0%{background-position:200% 0}to{background-position:-200% 0}}.detail-error[data-v-6235eb94]{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#8c8c8c}.error-icon[data-v-6235eb94]{width:48px;height:48px;color:#ff7875;margin-bottom:16px}.detail-body[data-v-6235eb94]{flex:1;display:flex;overflow:hidden}.detail-body.sidebar-collapsed .detail-content[data-v-6235eb94]{margin-left:0}.sidebar-resize-handle[data-v-6235eb94]{width:4px;flex-shrink:0;cursor:col-resize;background:transparent;transition:background .15s;margin-left:-2px;margin-right:-2px;position:relative;z-index:5}.sidebar-resize-handle[data-v-6235eb94]:hover,.sidebar-resize-handle[data-v-6235eb94]:active{background:#1890ff}.detail-sidebar[data-v-6235eb94]{width:100%;flex-shrink:0;border-right:1px solid #f0f0f0;background:#fff;display:flex;flex-direction:column;overflow:hidden}.sidebar-overview[data-v-6235eb94]{flex-shrink:0;padding:12px 16px;border:none;background:transparent;text-align:left;font-size:14px;cursor:pointer;color:#595959;transition:all .15s}.sidebar-overview[data-v-6235eb94]:hover{background:#fafafa}.sidebar-overview.active[data-v-6235eb94]{background:#1890ff14;color:#1890ff;font-weight:500}.sidebar-title[data-v-6235eb94]{flex-shrink:0;padding:12px 16px;border-bottom:1px solid #f0f0f0;border-top:1px solid #f0f0f0;font-size:14px;font-weight:600;color:#333}.sidebar-steps[data-v-6235eb94]{flex:1;overflow-y:auto;padding:8px 0}.agent-section[data-v-6235eb94]{margin-bottom:4px}.agent-header[data-v-6235eb94]{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;font-size:13px;font-weight:500;color:#333;cursor:pointer;text-align:left;transition:background .15s}.agent-header[data-v-6235eb94]:hover{background:#fafafa}.agent-icon-sm[data-v-6235eb94]{width:14px;height:14px;color:#1890ff}.agent-label[data-v-6235eb94]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.agent-tokens[data-v-6235eb94]{font-family:monospace;font-size:11px;color:#bfbfbf}.agent-count[data-v-6235eb94]{font-size:12px;color:#d9d9d9}.step-item[data-v-6235eb94]{display:flex;align-items:center;gap:8px;width:100%;padding:5px 16px 5px 40px;border:none;background:transparent;font-size:12px;line-height:20px;color:#595959;cursor:pointer;text-align:left;transition:all .15s}.step-item[data-v-6235eb94]:hover{background:#fafafa}.step-item.active[data-v-6235eb94]{background:#e6f7ff;color:#1890ff;font-weight:500}.step-num[data-v-6235eb94]{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;background:#f0f0f0;font-size:10px;font-weight:700;color:#8c8c8c;flex-shrink:0;line-height:1}.step-item.active .step-num[data-v-6235eb94]{background:#bae7ff;color:#0050b3}.step-text[data-v-6235eb94]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:20px}.step-dur[data-v-6235eb94]{font-family:monospace;font-size:11px;color:#bfbfbf;flex-shrink:0;line-height:20px}.detail-content[data-v-6235eb94]{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.compact-overview[data-v-6235eb94]{display:flex;align-items:center;gap:12px;background:#fff;border-radius:8px;padding:10px 16px;box-shadow:0 1px 3px #00000014;font-size:13px;flex-wrap:wrap}.compact-agent[data-v-6235eb94]{font-weight:500;color:#333}.compact-sep[data-v-6235eb94]{color:#d9d9d9}.compact-model[data-v-6235eb94],.compact-dur[data-v-6235eb94],.compact-tokens[data-v-6235eb94],.compact-tools[data-v-6235eb94]{color:#8c8c8c;font-size:12px}.compact-expand[data-v-6235eb94]{margin-left:auto;font-size:12px;color:#1890ff;background:none;border:none;cursor:pointer}.compact-expand[data-v-6235eb94]:hover{text-decoration:underline}.compact-icon[data-v-6235eb94]{width:14px;height:14px;vertical-align:-2px;margin-right:2px}.card-title-icon[data-v-6235eb94]{width:18px;height:18px;vertical-align:-3px;margin-right:4px}.build-icon[data-v-6235eb94]{width:16px;height:16px;vertical-align:-3px;margin-right:4px}.section-title-icon[data-v-6235eb94]{width:14px;height:14px;vertical-align:-2px;margin-right:4px}.cycle-status-icon[data-v-6235eb94]{width:14px;height:14px;vertical-align:-2px;flex-shrink:0}.cycle-status-icon.success[data-v-6235eb94]{color:#52c41a}.cycle-status-icon.fail[data-v-6235eb94]{color:#ff4d4f}.meta-icon[data-v-6235eb94]{width:12px;height:12px;vertical-align:-2px;margin-right:2px}.sidebar-overview-icon[data-v-6235eb94]{width:16px;height:16px;vertical-align:-3px;margin-right:4px;color:#1890ff}.scope-bar[data-v-6235eb94]{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}.scope-pill[data-v-6235eb94]{display:flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;border:1px solid #e8e8e8;background:#fff;font-size:12px;font-weight:500;color:#595959;cursor:pointer;transition:all .15s}.scope-pill.active[data-v-6235eb94]{border-color:transparent}.scope-dot[data-v-6235eb94]{width:8px;height:8px;border-radius:50%;flex-shrink:0}.card[data-v-6235eb94]{background:#fff;border-radius:12px;padding:20px;box-shadow:0 1px 3px #00000014}.card h2[data-v-6235eb94]{font-size:18px;font-weight:600;color:#333;margin:0 0 16px}.overview-grid[data-v-6235eb94]{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}.overview-item[data-v-6235eb94]{display:flex;align-items:center;gap:8px}.oi-label[data-v-6235eb94]{font-size:12px;color:#8c8c8c}.oi-value[data-v-6235eb94]{font-size:14px;font-weight:500;color:#333}.overview-dir[data-v-6235eb94]{margin-top:12px;background:#fafafa;padding:8px 12px;border-radius:4px;font-family:monospace;font-size:12px;color:#8c8c8c}.overview-date[data-v-6235eb94]{margin-top:8px;font-size:12px;color:#bfbfbf}.usage-row[data-v-6235eb94]{display:flex;align-items:center;gap:32px;margin-bottom:16px}.donut-wrap[data-v-6235eb94]{position:relative;flex-shrink:0}.donut-svg[data-v-6235eb94]{transform:rotate(-90deg)}.donut-center[data-v-6235eb94]{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center}.donut-total[data-v-6235eb94]{font-size:12px;font-weight:700;color:#333}.donut-label[data-v-6235eb94]{font-size:9px;color:#8c8c8c}.usage-legend[data-v-6235eb94]{display:grid;grid-template-columns:1fr 1fr;gap:6px 24px;font-size:12px}.legend-row[data-v-6235eb94]{display:flex;align-items:center;gap:6px}.legend-color[data-v-6235eb94]{width:10px;height:10px;border-radius:2px;flex-shrink:0}.legend-name[data-v-6235eb94]{color:#595959}.legend-val[data-v-6235eb94]{margin-left:auto;font-weight:500;color:#333}.usage-stats[data-v-6235eb94]{display:grid;grid-template-columns:repeat(6,1fr);gap:16px;text-align:center}.stat-val[data-v-6235eb94]{font-size:18px;font-weight:700;color:#333;display:block}.text-blue[data-v-6235eb94]{color:#1890ff}.text-purple[data-v-6235eb94]{color:#722ed1}.text-rose[data-v-6235eb94]{color:#eb2f96}.stat-lbl[data-v-6235eb94]{font-size:12px;color:#8c8c8c}.cache-rate[data-v-6235eb94]{display:flex;align-items:center;gap:8px;margin-top:12px}.cache-label[data-v-6235eb94]{font-size:12px;color:#8c8c8c}.cache-bar[data-v-6235eb94]{width:128px;height:8px;border-radius:4px;background:#e8e8e8}.cache-fill[data-v-6235eb94]{height:100%;border-radius:4px;background:#fa8c16;transition:width .5s}.cache-pct[data-v-6235eb94]{font-size:12px;font-weight:500;color:#fa8c16}.by-agent[data-v-6235eb94]{margin-top:20px;padding-top:16px;border-top:1px solid #f0f0f0}.by-agent h3[data-v-6235eb94]{font-size:12px;font-weight:600;text-transform:uppercase;color:#8c8c8c;margin:0 0 8px}.agent-bar-track[data-v-6235eb94]{display:flex;height:12px;border-radius:6px;overflow:hidden;background:#f0f0f0;margin-bottom:8px}.agent-bar-seg[data-v-6235eb94]{height:100%;transition:width .5s}.agent-rows[data-v-6235eb94]{display:flex;flex-direction:column;gap:6px}.agent-row[data-v-6235eb94]{display:flex;align-items:center;gap:8px;font-size:12px}.agent-dot[data-v-6235eb94]{width:10px;height:10px;border-radius:2px;flex-shrink:0}.agent-name[data-v-6235eb94]{flex:1;color:#595959;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.agent-val[data-v-6235eb94]{font-weight:500;color:#333}.agent-pct[data-v-6235eb94]{width:48px;text-align:right;color:#bfbfbf}.rounds-list[data-v-6235eb94]{display:flex;flex-direction:column;gap:12px}.round-item[data-v-6235eb94]{display:flex;gap:12px;background:#fafafa;border-radius:8px;padding:16px}.round-num[data-v-6235eb94]{width:28px;height:28px;border-radius:50%;background:#f0f5ff;color:#2f54eb;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0}.round-body[data-v-6235eb94]{flex:1;min-width:0}.round-msg[data-v-6235eb94]{font-size:14px;color:#333;margin:0;white-space:pre-wrap}.round-msg-empty[data-v-6235eb94]{color:#bfbfbf;font-style:italic}.round-msg-list[data-v-6235eb94]{display:flex;flex-direction:column;gap:6px}.round-meta[data-v-6235eb94]{display:flex;flex-wrap:wrap;gap:16px;margin-top:8px;font-size:12px;color:#8c8c8c}.build-banner[data-v-6235eb94]{display:flex;align-items:center;gap:12px;border-radius:8px;padding:12px 16px;margin-bottom:16px;font-size:14px;font-weight:500}.build-success[data-v-6235eb94]{background:#f6ffed;color:#52c41a}.build-fail[data-v-6235eb94]{background:#fff1f0;color:#ff4d4f}.build-stat[data-v-6235eb94]{font-size:12px;color:#595959;margin-left:auto}.build-errors[data-v-6235eb94]{font-size:12px;color:#ff4d4f;font-weight:600}.error-codes h3[data-v-6235eb94],.fix-cycles h3[data-v-6235eb94],.warnings h3[data-v-6235eb94]{font-size:12px;font-weight:600;text-transform:uppercase;color:#8c8c8c;margin:0 0 12px}.error-row[data-v-6235eb94]{display:flex;align-items:center;gap:8px;padding:4px 6px;border-radius:4px;transition:background .15s}.error-row[data-v-6235eb94]:hover{background:#fff1f0}.err-code[data-v-6235eb94]{width:120px;flex-shrink:0;font-family:monospace;font-size:12px;color:#ff4d4f;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.err-msg[data-v-6235eb94]{flex:1;font-size:12px;color:#595959;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.err-bar[data-v-6235eb94]{width:96px;height:12px;border-radius:3px;background:#f0f0f0;overflow:hidden;flex-shrink:0}.err-fill[data-v-6235eb94]{height:100%;border-radius:3px;background:#ff7875;display:flex;align-items:center;justify-content:flex-end;padding-right:4px}.err-fill span[data-v-6235eb94]{font-size:9px;font-weight:500;color:#fff}.fix-cycles[data-v-6235eb94]{margin-top:16px}.cycle-stats[data-v-6235eb94]{display:flex;gap:24px;margin-bottom:12px}.cycle-val[data-v-6235eb94]{font-size:18px;font-weight:700;display:block}.text-green[data-v-6235eb94]{color:#52c41a}.text-amber[data-v-6235eb94]{color:#fa8c16}.cycle-lbl[data-v-6235eb94]{font-size:10px;color:#8c8c8c}.cycle-row[data-v-6235eb94]{display:flex;align-items:center;gap:8px;background:#fafafa;padding:6px 8px;border-radius:4px;font-size:12px;color:#595959;margin-bottom:4px}.cycle-dur[data-v-6235eb94]{margin-left:auto;font-family:monospace;color:#bfbfbf}.warnings[data-v-6235eb94]{margin-top:16px}.warn-type-row[data-v-6235eb94]{display:flex;align-items:center;gap:8px;margin-bottom:4px}.warn-type[data-v-6235eb94]{font-size:12px;color:#595959;text-transform:capitalize;width:120px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.warn-bar[data-v-6235eb94]{flex:1;height:12px;border-radius:3px;background:#f0f0f0;overflow:hidden}.warn-fill[data-v-6235eb94]{height:100%;border-radius:3px;background:#ffc53d}.warn-count[data-v-6235eb94]{font-size:10px;color:#8c8c8c;width:16px;text-align:right}.warn-entry[data-v-6235eb94]{background:#fffbe6;padding:6px 8px;border-radius:4px;margin-bottom:4px}.warn-file[data-v-6235eb94]{font-family:monospace;font-size:10px;color:#8c8c8c;display:block}.warn-msg[data-v-6235eb94]{font-size:12px;color:#595959}.waterfall[data-v-6235eb94]{display:flex;flex-direction:column;gap:16px}.mobile-steps[data-v-6235eb94]{display:none}@media(max-width:1024px){.detail-sidebar[data-v-6235eb94]{display:none}.mobile-steps[data-v-6235eb94]{display:block}.mobile-steps h2[data-v-6235eb94]{font-size:18px;font-weight:600;color:#333;margin:0 0 12px}.mobile-step[data-v-6235eb94]{display:flex;align-items:center;gap:12px;width:100%;background:#fff;border-radius:8px;padding:12px;box-shadow:0 1px 3px #00000014;margin-bottom:8px;border:none;cursor:pointer;text-align:left;transition:background .15s}.mobile-step[data-v-6235eb94]:hover{background:#fafafa}.mobile-step-num[data-v-6235eb94]{width:28px;height:28px;border-radius:50%;background:#e6f7ff;color:#1890ff;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0}.mobile-step-body[data-v-6235eb94]{flex:1;min-width:0}.mobile-step-text[data-v-6235eb94]{font-size:14px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mobile-step-meta[data-v-6235eb94]{display:flex;gap:12px;margin-top:4px;font-size:12px;color:#8c8c8c}}.overview-skills[data-v-6235eb94]{margin-top:16px;padding-top:16px;border-top:1px solid #f0f0f0}.skills-title[data-v-6235eb94]{font-size:12px;font-weight:600;text-transform:uppercase;color:#8c8c8c;margin:0 0 8px}.skills-list[data-v-6235eb94]{display:flex;flex-direction:column;gap:4px}.skill-row[data-v-6235eb94]{display:flex;align-items:center;gap:8px;padding:6px 8px;background:#fafafa;border-radius:4px;font-size:13px}.skill-icon[data-v-6235eb94]{width:14px;height:14px;flex-shrink:0}.skill-icon.completed[data-v-6235eb94]{color:#52c41a}.skill-icon.running[data-v-6235eb94]{color:#1890ff}.skill-icon.failed[data-v-6235eb94]{color:#ff4d4f}.skill-name[data-v-6235eb94]{flex:1;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.skill-status[data-v-6235eb94]{font-size:12px;flex-shrink:0}.skill-status.completed[data-v-6235eb94]{color:#52c41a}.skill-status.running[data-v-6235eb94]{color:#1890ff}.skill-status.failed[data-v-6235eb94]{color:#ff4d4f}.overview-system-prompts[data-v-6235eb94]{margin-top:16px;padding-top:16px;border-top:1px solid #f0f0f0}.sp-title[data-v-6235eb94]{font-size:12px;font-weight:600;text-transform:uppercase;color:#8c8c8c;margin:0 0 8px}.sp-list[data-v-6235eb94]{display:flex;flex-direction:column;gap:4px}.sp-model[data-v-6235eb94]{border-radius:4px;overflow:hidden}.sp-model-header[data-v-6235eb94]{display:flex;align-items:center;gap:8px;width:100%;padding:6px 8px;border:none;background:#fafafa;font-size:13px;cursor:pointer;text-align:left;transition:background .15s}.sp-model-header[data-v-6235eb94]:hover{background:#f0f0f0}.sp-model-name[data-v-6235eb94]{flex:1;font-weight:500;color:#333}.sp-model-count[data-v-6235eb94]{font-size:11px;color:#bfbfbf}.sp-contents[data-v-6235eb94]{display:flex;flex-direction:column;gap:4px;padding:4px 0}.sp-content[data-v-6235eb94]{background:#fafafa;border-radius:4px;padding:8px 12px}.sp-text[data-v-6235eb94]{margin:0;font-family:monospace;font-size:12px;color:#595959;white-space:pre-wrap;word-break:break-all;line-height:1.6}.planning-content[data-v-6235eb94]{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.planning-summary[data-v-6235eb94]{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}.summary-card[data-v-6235eb94]{background:#fff;border-radius:12px;padding:16px;box-shadow:0 1px 3px #00000014;display:flex;flex-direction:column;align-items:center;gap:4px}.summary-val[data-v-6235eb94]{font-size:18px;font-weight:700;color:#333}.summary-lbl[data-v-6235eb94]{font-size:12px;color:#8c8c8c}.summary-card-priority[data-v-6235eb94]{padding:12px 16px;gap:6px}.priority-counts[data-v-6235eb94]{display:flex;flex-direction:column;gap:4px}.priority-item[data-v-6235eb94]{display:flex;align-items:center;gap:6px;font-size:13px;color:#333;font-weight:500}.legend-dot-sm[data-v-6235eb94]{display:inline-block;width:8px;height:8px;border-radius:2px}.rounds-section[data-v-6235eb94]{background:#fff;border-radius:12px;padding:20px;box-shadow:0 1px 3px #00000014}.rounds-header[data-v-6235eb94]{display:flex;align-items:center;gap:8px;width:100%;border:none;background:transparent;cursor:pointer;padding:0;text-align:left}.rounds-title[data-v-6235eb94]{font-size:18px;font-weight:600;color:#333}.rounds-body[data-v-6235eb94]{margin-top:16px;display:flex;flex-direction:column;gap:12px}.round-block[data-v-6235eb94]{background:#fafafa;border-radius:8px;padding:12px 16px}.round-header[data-v-6235eb94]{display:flex;align-items:baseline;gap:16px;margin-bottom:8px}.round-num[data-v-6235eb94]{font-size:13px;font-weight:600;color:#333;flex-shrink:0;min-width:56px}.round-time[data-v-6235eb94]{font-family:monospace;font-size:12px;color:#8c8c8c;flex-shrink:0}.round-dur[data-v-6235eb94]{font-family:monospace;font-size:11px;color:#bfbfbf;margin-left:auto}.round-progress-mini[data-v-6235eb94]{display:flex;height:4px;border-radius:2px;overflow:hidden;background:#e8e8e8;margin-bottom:8px}.round-progress-seg[data-v-6235eb94]{height:100%;transition:width .3s}.round-progress-seg.completed[data-v-6235eb94]{background:#52c41a}.round-progress-seg.in-progress[data-v-6235eb94]{background:#3b82f6}.round-todos[data-v-6235eb94]{display:flex;flex-direction:column;gap:2px}.todo-row[data-v-6235eb94]{display:flex;align-items:center;gap:8px;padding:4px 6px;border-radius:4px;font-size:13px}.todo-row[data-v-6235eb94]:hover{background:#f0f0f0}.todo-icon[data-v-6235eb94]{width:14px;height:14px;flex-shrink:0}.todo-icon.completed[data-v-6235eb94]{color:#52c41a}.todo-icon.in-progress[data-v-6235eb94]{color:#1890ff;fill:#1890ff}.todo-icon.pending[data-v-6235eb94]{color:#d9d9d9}.todo-content[data-v-6235eb94]{flex:1;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.todo-meta[data-v-6235eb94]{font-size:11px;color:#8c8c8c;flex-shrink:0}.filter-empty[data-v-6235eb94]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px;color:#8c8c8c}.filter-empty p[data-v-6235eb94]{font-size:14px;margin:0 0 12px}.filter-empty-reset[data-v-6235eb94]{font-size:13px;color:#1890ff;background:none;border:none;cursor:pointer}.filter-empty-reset[data-v-6235eb94]:hover{text-decoration:underline}.cloud-file-list[data-v-fcef4675]{padding:20px;height:100%;display:flex;flex-direction:column;gap:16px}.list-header[data-v-fcef4675]{display:flex;align-items:center;justify-content:space-between;gap:16px}.header-title h2[data-v-fcef4675]{font-size:18px;color:#303133}.header-sub[data-v-fcef4675]{font-size:13px;color:#909399;margin-top:2px}.header-actions[data-v-fcef4675]{display:flex;align-items:center;gap:8px}.prefix-tag[data-v-fcef4675]{flex-shrink:0}.prefix-input[data-v-fcef4675]{width:280px}.file-table[data-v-fcef4675]{flex:1;min-height:300px}.row-name[data-v-fcef4675]{display:inline-flex;align-items:center;gap:8px;min-width:0;font-size:14.5px}.folder-name[data-v-fcef4675]{font-weight:500}.row-icon[data-v-fcef4675]{font-size:18px;color:#909399;flex-shrink:0}.folder-name .row-icon[data-v-fcef4675]{color:#e6a23c}[data-v-fcef4675] .folder-row{cursor:pointer}.action-btn[data-v-fcef4675]{font-size:17px}.list-footer[data-v-fcef4675]{display:flex;justify-content:flex-end}.page-indicator[data-v-fcef4675]{font-size:13px;color:#606266;padding:0 12px}.meta-file-name[data-v-fcef4675]{font-size:13px;color:#606266;word-break:break-all}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "session-viewer-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "会话指标查看插件:可嵌入 Vue3 + Element Plus 宿主的视图组件库,搭配独立伴生服务提供本地会话快照浏览与 AGC 云端文件管理。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"session-viewer-plugin": "server/dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
"./ui": "./frontend/dist-ui/index.mjs",
|
|
12
|
+
"./ui.css": "./frontend/dist-ui/style.css"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"server/dist",
|
|
16
|
+
"!server/dist/assets",
|
|
17
|
+
"frontend/dist-ui",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"setup": "cd server && npm install && cd ../frontend && npm install",
|
|
22
|
+
"dev": "concurrently \"cd server && npm run dev\" \"cd frontend && npm run dev\"",
|
|
23
|
+
"dev:server": "cd server && npm run dev",
|
|
24
|
+
"dev:frontend": "cd frontend && npm run dev",
|
|
25
|
+
"build": "npm run build:server && npm run build:ui",
|
|
26
|
+
"build:server": "cd server && npx tsc && node -e \"require('fs').cpSync('src/assets','dist/assets',{recursive:true})\"",
|
|
27
|
+
"build:ui": "cd frontend && npx vite build --config vite.lib.config.js",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"chokidar": "^4.0.3",
|
|
32
|
+
"cors": "^2.8.5",
|
|
33
|
+
"env-paths": "^4.0.0",
|
|
34
|
+
"express": "^5.1.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@element-plus/icons-vue": "^2.3.0",
|
|
38
|
+
"element-plus": "^2.4.0",
|
|
39
|
+
"vue": "^3.4.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"@element-plus/icons-vue": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"element-plus": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"vue": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"concurrently": "^9.2.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { CloudStorageError } from '../services/cloudError.js';
|
|
4
|
+
import { listCloudFiles, listAllCloudFiles, downloadCloudFile, getCloudFileDownloadToken, deleteCloudFile, getCloudFileMetadata, updateCloudFileMetadata, cloudFileExists, renameCloudFile, } from '../services/cloudStorage.js';
|
|
5
|
+
// 模糊查询返回上限与文件夹单次删除上限(桶内对象量级 1 万以内)
|
|
6
|
+
const SEARCH_RESULT_LIMIT = 500;
|
|
7
|
+
const FOLDER_DELETE_LIMIT = 5000;
|
|
8
|
+
function sendCloudError(res, err, fallback) {
|
|
9
|
+
if (err instanceof CloudStorageError) {
|
|
10
|
+
console.error(`Cloud storage error: ${err.message}`);
|
|
11
|
+
res.status(err.status).json({ error: err.message });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.error(fallback + ':', err);
|
|
15
|
+
res.status(500).json({ error: fallback });
|
|
16
|
+
}
|
|
17
|
+
function requireKey(req) {
|
|
18
|
+
const key = typeof req.query.key === 'string' ? req.query.key.trim() : '';
|
|
19
|
+
return key || null;
|
|
20
|
+
}
|
|
21
|
+
export const cloudApi = express.Router();
|
|
22
|
+
cloudApi.get('/cloud/files', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const prefix = typeof req.query.prefix === 'string' ? req.query.prefix : '';
|
|
25
|
+
const marker = typeof req.query.marker === 'string' ? req.query.marker : undefined;
|
|
26
|
+
const delimiterRaw = String(req.query.delimiter ?? '');
|
|
27
|
+
const delimiter = delimiterRaw !== '' && delimiterRaw !== '0' && delimiterRaw !== 'false';
|
|
28
|
+
const limitRaw = Number(req.query.limit);
|
|
29
|
+
const limit = Number.isFinite(limitRaw)
|
|
30
|
+
? Math.min(Math.max(Math.trunc(limitRaw), 1), 1000)
|
|
31
|
+
: 100;
|
|
32
|
+
const result = await listCloudFiles({ prefix, marker, limit, delimiter });
|
|
33
|
+
console.log(`GET /api/cloud/files, prefix: '${prefix}', delimiter: ${delimiter}, limit: ${limit}, returning ${result.folders.length} folders, ${result.files.length} files`);
|
|
34
|
+
res.json(result);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
sendCloudError(res, err, 'Failed to list cloud files');
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
cloudApi.get('/cloud/file/download', async (req, res) => {
|
|
41
|
+
const key = requireKey(req);
|
|
42
|
+
if (!key) {
|
|
43
|
+
res.status(400).json({ error: 'Missing required query param: key' });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const dl = await downloadCloudFile(key);
|
|
48
|
+
res.setHeader('Content-Type', dl.contentType);
|
|
49
|
+
if (dl.contentLength)
|
|
50
|
+
res.setHeader('Content-Length', dl.contentLength);
|
|
51
|
+
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(dl.filename)}"; filename*=UTF-8''${encodeURIComponent(dl.filename)}`);
|
|
52
|
+
if (dl.body) {
|
|
53
|
+
const stream = Readable.fromWeb(dl.body);
|
|
54
|
+
stream.pipe(res);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
res.end();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
sendCloudError(res, err, 'Failed to download cloud file');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// ─── 下载令牌(AGC 官方 ?token=create,公网免鉴权下载 URL)────
|
|
65
|
+
cloudApi.get('/cloud/file/token', async (req, res) => {
|
|
66
|
+
const key = requireKey(req);
|
|
67
|
+
if (!key) {
|
|
68
|
+
res.status(400).json({ error: 'Missing required query param: key' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const result = await getCloudFileDownloadToken(key);
|
|
73
|
+
console.log(`GET /api/cloud/file/token, key: ${key}, token: ${result.token}`);
|
|
74
|
+
res.json(result);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
sendCloudError(res, err, 'Failed to create cloud file token');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
cloudApi.delete('/cloud/file', async (req, res) => {
|
|
81
|
+
const key = requireKey(req);
|
|
82
|
+
if (!key) {
|
|
83
|
+
res.status(400).json({ error: 'Missing required query param: key' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await deleteCloudFile(key);
|
|
88
|
+
console.log(`DELETE /api/cloud/file, key: ${key}, deleted`);
|
|
89
|
+
res.json({ deleted: true, key });
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
sendCloudError(res, err, 'Failed to delete cloud file');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// ─── 模糊查询(AGC 无模糊 API:服务端全量拉取 + 内存 contains 匹配)─────────
|
|
96
|
+
cloudApi.get('/cloud/search', async (req, res) => {
|
|
97
|
+
const query = typeof req.query.query === 'string' ? req.query.query.trim() : '';
|
|
98
|
+
const prefix = typeof req.query.prefix === 'string' ? req.query.prefix.trim() : '';
|
|
99
|
+
if (!query) {
|
|
100
|
+
res.status(400).json({ error: 'Missing required query param: query' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const all = await listAllCloudFiles(prefix);
|
|
105
|
+
const q = query.toLowerCase();
|
|
106
|
+
const hits = all.filter((f) => f.name.toLowerCase().includes(q));
|
|
107
|
+
const truncated = hits.length > SEARCH_RESULT_LIMIT;
|
|
108
|
+
const files = truncated ? hits.slice(0, SEARCH_RESULT_LIMIT) : hits;
|
|
109
|
+
console.log(`GET /api/cloud/search, query: '${query}', prefix: '${prefix}', scanned ${all.length}, hits ${hits.length}`);
|
|
110
|
+
res.json({ total: hits.length, truncated, scanned: all.length, files });
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
sendCloudError(res, err, 'Failed to search cloud files');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// ─── 文件夹(前缀)预估与递归删除 ──────────────────────────────
|
|
117
|
+
// 合法文件夹前缀:非空、以 / 结尾、不含 ..,避免误删整桶或越权
|
|
118
|
+
function requireFolderPrefix(req) {
|
|
119
|
+
const prefix = typeof req.query.prefix === 'string' ? req.query.prefix.trim() : '';
|
|
120
|
+
if (!prefix || !prefix.endsWith('/') || prefix.includes('..'))
|
|
121
|
+
return null;
|
|
122
|
+
return prefix;
|
|
123
|
+
}
|
|
124
|
+
cloudApi.get('/cloud/folder/count', async (req, res) => {
|
|
125
|
+
const prefix = requireFolderPrefix(req);
|
|
126
|
+
if (!prefix) {
|
|
127
|
+
res.status(400).json({ error: 'prefix 必须非空且以 / 结尾' });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const files = await listAllCloudFiles(prefix);
|
|
132
|
+
res.json({ count: files.length });
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
sendCloudError(res, err, 'Failed to count cloud folder');
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
cloudApi.delete('/cloud/folder', async (req, res) => {
|
|
139
|
+
const prefix = requireFolderPrefix(req);
|
|
140
|
+
if (!prefix) {
|
|
141
|
+
res.status(400).json({ error: 'prefix 必须非空且以 / 结尾' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const files = await listAllCloudFiles(prefix, 5);
|
|
146
|
+
if (files.length === 0) {
|
|
147
|
+
res.json({ deleted: 0, failed: 0, errors: [] });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (files.length > FOLDER_DELETE_LIMIT) {
|
|
151
|
+
res.status(400).json({
|
|
152
|
+
error: `该前缀下共 ${files.length} 个对象,超过单次删除上限 ${FOLDER_DELETE_LIMIT},请缩小范围`,
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// 小并发逐个删除(AGC 无批量删除接口;并发过大会触发限流)
|
|
157
|
+
const failed = [];
|
|
158
|
+
const queue = files.map((f) => f.name);
|
|
159
|
+
let deleted = 0;
|
|
160
|
+
async function worker() {
|
|
161
|
+
for (;;) {
|
|
162
|
+
const key = queue.shift();
|
|
163
|
+
if (key === undefined)
|
|
164
|
+
return;
|
|
165
|
+
try {
|
|
166
|
+
await deleteCloudFile(key);
|
|
167
|
+
deleted++;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
failed.push(key);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
await Promise.all(Array.from({ length: Math.min(5, files.length) }, worker));
|
|
175
|
+
console.log(`DELETE /api/cloud/folder, prefix: ${prefix}, deleted ${deleted}, failed ${failed.length}`);
|
|
176
|
+
res.json({ deleted, failed: failed.length, errors: failed.slice(0, 10) });
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
sendCloudError(res, err, 'Failed to delete cloud folder');
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
cloudApi.put('/cloud/file/rename', async (req, res) => {
|
|
183
|
+
const key = requireKey(req);
|
|
184
|
+
if (!key) {
|
|
185
|
+
res.status(400).json({ error: 'Missing required query param: key' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const name = typeof req.body?.name === 'string' ? req.body.name.trim() : '';
|
|
189
|
+
if (!name) {
|
|
190
|
+
res.status(400).json({ error: 'Missing required body field: name' });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (name.includes('/')) {
|
|
194
|
+
res.status(400).json({ error: '文件名不能包含 "/"' });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const dir = key.includes('/') ? key.slice(0, key.lastIndexOf('/') + 1) : '';
|
|
198
|
+
const newKey = `${dir}${name}`;
|
|
199
|
+
if (newKey === key) {
|
|
200
|
+
res.status(400).json({ error: '新文件名与原文件名相同' });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (await cloudFileExists(newKey)) {
|
|
204
|
+
res.status(409).json({ error: `目标文件已存在:${newKey}` });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
await renameCloudFile(key, newKey);
|
|
209
|
+
console.log(`PUT /api/cloud/file/rename, ${key} -> ${newKey}`);
|
|
210
|
+
res.json({ renamed: true, key: newKey });
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
sendCloudError(res, err, 'Failed to rename cloud file');
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
cloudApi.get('/cloud/file/metadata', async (req, res) => {
|
|
217
|
+
const key = requireKey(req);
|
|
218
|
+
if (!key) {
|
|
219
|
+
res.status(400).json({ error: 'Missing required query param: key' });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const metadata = await getCloudFileMetadata(key);
|
|
224
|
+
res.json(metadata);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
sendCloudError(res, err, 'Failed to get cloud file metadata');
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
cloudApi.put('/cloud/file/metadata', async (req, res) => {
|
|
231
|
+
const key = requireKey(req);
|
|
232
|
+
if (!key) {
|
|
233
|
+
res.status(400).json({ error: 'Missing required query param: key' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const body = (req.body ?? {});
|
|
238
|
+
const update = {};
|
|
239
|
+
const stringFields = [
|
|
240
|
+
'contentType',
|
|
241
|
+
'contentDisposition',
|
|
242
|
+
'contentEncoding',
|
|
243
|
+
'cacheControl',
|
|
244
|
+
'contentLanguage',
|
|
245
|
+
];
|
|
246
|
+
for (const field of stringFields) {
|
|
247
|
+
const v = body[field];
|
|
248
|
+
if (typeof v === 'string' && v.trim())
|
|
249
|
+
update[field] = v.trim();
|
|
250
|
+
}
|
|
251
|
+
if (body.customMetadata && typeof body.customMetadata === 'object' && !Array.isArray(body.customMetadata)) {
|
|
252
|
+
const meta = {};
|
|
253
|
+
for (const [k, v] of Object.entries(body.customMetadata)) {
|
|
254
|
+
if (k.trim())
|
|
255
|
+
meta[k.trim()] = String(v ?? '');
|
|
256
|
+
}
|
|
257
|
+
if (Object.keys(meta).length)
|
|
258
|
+
update.customMetadata = meta;
|
|
259
|
+
}
|
|
260
|
+
const metadata = await updateCloudFileMetadata(key, update);
|
|
261
|
+
console.log(`PUT /api/cloud/file/metadata, key: ${key}, updated fields:`, Object.keys(update));
|
|
262
|
+
res.json(metadata);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
sendCloudError(res, err, 'Failed to update cloud file metadata');
|
|
266
|
+
}
|
|
267
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { getConfig, saveConfig, toggleFavorite } from '../services/config.js';
|
|
3
|
+
export const configApi = express.Router();
|
|
4
|
+
configApi.get('/config', async (_req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
const config = await getConfig();
|
|
7
|
+
res.json(config);
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
console.error('Error reading config:', err);
|
|
11
|
+
res.status(500).json({ error: 'Failed to read config' });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
configApi.put('/config', async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const { favorites, customDirs } = req.body;
|
|
17
|
+
const config = {
|
|
18
|
+
favorites: Array.isArray(favorites) ? favorites.map(String) : [],
|
|
19
|
+
customDirs: Array.isArray(customDirs) ? customDirs.map(String) : [],
|
|
20
|
+
};
|
|
21
|
+
await saveConfig(config);
|
|
22
|
+
res.json(config);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.error('Error saving config:', err);
|
|
26
|
+
res.status(500).json({ error: 'Failed to save config' });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
configApi.put('/config/favorites/:id', async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const favorites = await toggleFavorite(req.params.id);
|
|
32
|
+
res.json({ favorites });
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.error('Error toggling favorite:', err);
|
|
36
|
+
res.status(500).json({ error: 'Failed to toggle favorite' });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import envPaths from "env-paths";
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import { getSessions, getSessionDetail, invalidateCache } from '../services/assembler.js';
|
|
6
|
+
const paths = envPaths("hm-plugin");
|
|
7
|
+
const ompPaths = envPaths("opencode-metrics-plugin", { suffix: "" });
|
|
8
|
+
export const DEFAULT_DIRS = process.env.LOG_DIR
|
|
9
|
+
? [process.env.LOG_DIR]
|
|
10
|
+
: [
|
|
11
|
+
path.join(paths.log, 'metrics'),
|
|
12
|
+
// opencode-metrics-plugin 0.2.0 默认输出目录(env-paths 无 suffix,与插件端 defaultDirs 一致)
|
|
13
|
+
path.join(ompPaths.log, 'metrics'),
|
|
14
|
+
];
|
|
15
|
+
function resolveDirs(req) {
|
|
16
|
+
const customDirs = req.query.dirs;
|
|
17
|
+
const customs = Array.isArray(customDirs)
|
|
18
|
+
? customDirs.map(String)
|
|
19
|
+
: customDirs
|
|
20
|
+
? [String(customDirs)]
|
|
21
|
+
: [];
|
|
22
|
+
return [...DEFAULT_DIRS, ...customs];
|
|
23
|
+
}
|
|
24
|
+
export const sessionApi = express.Router();
|
|
25
|
+
sessionApi.get('/directories', (_req, res) => {
|
|
26
|
+
res.json({ defaults: DEFAULT_DIRS });
|
|
27
|
+
});
|
|
28
|
+
sessionApi.get('/sessions', async (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const dirs = resolveDirs(req);
|
|
31
|
+
console.log('GET /api/sessions called, dirs:', dirs);
|
|
32
|
+
const sessions = await getSessions(dirs);
|
|
33
|
+
console.log('Returning', sessions.length, 'sessions');
|
|
34
|
+
res.json(sessions);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.error('Error listing sessions:', err);
|
|
38
|
+
res.status(500).json({ error: 'Failed to list sessions' });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
sessionApi.get('/sessions/:id', async (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const dirs = resolveDirs(req);
|
|
44
|
+
const detail = await getSessionDetail(dirs, req.params.id);
|
|
45
|
+
if (!detail) {
|
|
46
|
+
res.status(404).json({ error: 'Session not found' });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
res.json(detail);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.error('Error getting session detail:', err);
|
|
53
|
+
res.status(500).json({ error: 'Failed to get session detail' });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
sessionApi.get('/sessions/:id/download', async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const dirs = resolveDirs(req);
|
|
59
|
+
const sessionId = req.params.id;
|
|
60
|
+
for (const dir of dirs) {
|
|
61
|
+
const filePath = path.resolve(dir, `${sessionId}.json`);
|
|
62
|
+
if (!filePath.startsWith(path.resolve(dir) + path.sep))
|
|
63
|
+
continue;
|
|
64
|
+
try {
|
|
65
|
+
await fs.access(filePath);
|
|
66
|
+
res.setHeader('Content-Type', 'application/json');
|
|
67
|
+
res.setHeader('Content-Disposition', `attachment; filename="${sessionId}.json"`);
|
|
68
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
69
|
+
res.send(content);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// File not found in this dir, try next
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
res.status(404).json({ error: 'Session file not found' });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error('Error downloading session:', err);
|
|
80
|
+
res.status(500).json({ error: 'Failed to download session' });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
sessionApi.delete('/sessions/:id', async (req, res) => {
|
|
84
|
+
try {
|
|
85
|
+
const id = req.params.id;
|
|
86
|
+
const filePath = req.body?.filePath;
|
|
87
|
+
const dirs = resolveDirs(req);
|
|
88
|
+
let metricsDeleted = false;
|
|
89
|
+
let logDeleted = false;
|
|
90
|
+
let metricsFile = '';
|
|
91
|
+
let logFile = '';
|
|
92
|
+
// Validate filePath is under one of the allowed directories
|
|
93
|
+
function isPathAllowed(targetPath) {
|
|
94
|
+
const resolved = path.resolve(targetPath);
|
|
95
|
+
return dirs.some((dir) => {
|
|
96
|
+
const dirResolved = path.resolve(dir);
|
|
97
|
+
return resolved.startsWith(dirResolved + path.sep) || resolved === dirResolved;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (filePath && isPathAllowed(filePath)) {
|
|
101
|
+
const logPath = path.join(path.dirname(filePath), '..', 'events', `${id}.log`);
|
|
102
|
+
try {
|
|
103
|
+
await fs.unlink(filePath);
|
|
104
|
+
metricsDeleted = true;
|
|
105
|
+
metricsFile = filePath;
|
|
106
|
+
console.log('Deleted metrics file:', filePath);
|
|
107
|
+
}
|
|
108
|
+
catch { }
|
|
109
|
+
try {
|
|
110
|
+
await fs.unlink(logPath);
|
|
111
|
+
logDeleted = true;
|
|
112
|
+
logFile = logPath;
|
|
113
|
+
console.log('Deleted log file:', logPath);
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Fallback: search through all directories
|
|
119
|
+
for (const dir of dirs) {
|
|
120
|
+
const metricsPath = path.join(dir, `${id}.json`);
|
|
121
|
+
const logPath = path.join(dir, '..', 'events', `${id}.log`);
|
|
122
|
+
try {
|
|
123
|
+
await fs.access(metricsPath);
|
|
124
|
+
await fs.unlink(metricsPath);
|
|
125
|
+
metricsDeleted = true;
|
|
126
|
+
metricsFile = metricsPath;
|
|
127
|
+
console.log('Deleted metrics file:', metricsPath);
|
|
128
|
+
}
|
|
129
|
+
catch { }
|
|
130
|
+
try {
|
|
131
|
+
await fs.access(logPath);
|
|
132
|
+
await fs.unlink(logPath);
|
|
133
|
+
logDeleted = true;
|
|
134
|
+
logFile = logPath;
|
|
135
|
+
console.log('Deleted log file:', logPath);
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!metricsDeleted && !logDeleted) {
|
|
141
|
+
res.status(404).json({ error: 'Session not found' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
invalidateCache();
|
|
145
|
+
res.json({ deleted: true, metricsFile, logFile });
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error('Error deleting session:', err);
|
|
149
|
+
res.status(500).json({ error: 'Failed to delete session' });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// 应用工厂:组装 Express 实例(中间件与全部 API 路由),供开发入口与 bin 启动入口复用。
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import { sessionApi } from './api/sessionApi.js';
|
|
5
|
+
import { configApi } from './api/configApi.js';
|
|
6
|
+
import { cloudApi } from './api/cloudApi.js';
|
|
7
|
+
/**
|
|
8
|
+
* 创建组装完成的 Express 应用(未监听端口)。
|
|
9
|
+
* @returns 可直接调用 listen 的 Express 实例
|
|
10
|
+
*/
|
|
11
|
+
export function createApp() {
|
|
12
|
+
const app = express();
|
|
13
|
+
app.use(cors());
|
|
14
|
+
app.use(express.json());
|
|
15
|
+
app.use('/api', sessionApi);
|
|
16
|
+
app.use('/api', configApi);
|
|
17
|
+
app.use('/api', cloudApi);
|
|
18
|
+
return app;
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 服务启动入口:读取环境变量并启动 HTTP 服务,可由 node/tsx 直接执行(npm bin 脚本)。
|
|
3
|
+
// 环境变量:PORT 监听端口(默认 3001);HOST 绑定地址(默认 127.0.0.1,容器部署需设为 0.0.0.0);
|
|
4
|
+
// LOG_DIR 覆盖默认扫描目录;AGC_CREDENTIALS_FILE/AGC_CREDENTIALS_JSON 覆盖云端凭证。
|
|
5
|
+
import { createApp } from './app.js';
|
|
6
|
+
import { DEFAULT_DIRS } from './api/sessionApi.js';
|
|
7
|
+
import { ensureDataDir } from './services/config.js';
|
|
8
|
+
const PORT = Number(process.env.PORT) || 3001;
|
|
9
|
+
const HOST = process.env.HOST || '127.0.0.1';
|
|
10
|
+
await ensureDataDir();
|
|
11
|
+
const app = createApp();
|
|
12
|
+
app.listen(PORT, HOST, () => {
|
|
13
|
+
console.log(`Server running on http://${HOST}:${PORT}`);
|
|
14
|
+
console.log('Scan directories:', DEFAULT_DIRS);
|
|
15
|
+
});
|