apiskill 0.1.5 → 0.1.6
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 +10 -0
- package/README.zh.md +12 -0
- package/dist/assets/index-B_wAJLu_.js +324 -0
- package/dist/assets/index-CHOQ6xhP.css +1 -0
- package/dist/index.html +2 -2
- package/docs/cli.md +13 -0
- package/docs/cli.zh.md +13 -0
- package/docs/web.md +2 -0
- package/docs/web.zh.md +2 -0
- package/package.json +3 -3
- package/scripts/apiskill-cli.mjs +46 -2
- package/scripts/lib/cache-paths.mjs +94 -3
- package/scripts/test-shared-cache.mjs +42 -7
- package/src/App.tsx +29 -0
- package/src/CacheSettingsDialog.tsx +166 -0
- package/src/DocumentVersionManager.tsx +47 -1
- package/src/styles.css +119 -0
- package/src/swagger.ts +2 -0
- package/vite.config.ts +92 -1
- package/dist/assets/index-CV6e3HAP.js +0 -299
- package/dist/assets/index-slFOragU.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{color-scheme:light;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:#eef1f4;color:#18202a;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased}*{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh}button,input,select{font:inherit}button{cursor:pointer}.app-shell{display:flex;flex-direction:column;height:100svh;min-height:100vh;padding:18px 18px 0;overflow:hidden}.topbar{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:16px 20px;border:1px solid #d7dde4;border-radius:8px;background:#fff}.eyebrow{margin:0 0 4px;color:#667282;font-size:12px;font-weight:700;letter-spacing:0;text-transform:uppercase}h1,h2,h3,h4,p{margin-top:0}.topbar h1{margin:0;font-size:24px;line-height:1.2}.topbar-actions{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:14px;min-width:0}.version-switcher{display:inline-flex;align-items:center;gap:8px;color:#647182;font-size:12px;font-weight:800}.version-switcher select{width:min(320px,32vw);min-height:36px;padding:0 10px;border:1px solid #cfd7e1;border-radius:6px;outline:0;color:#15202c;background:#fff}.version-manager{min-width:min(420px,38vw)}.version-manager-trigger{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:9px;width:100%;min-height:44px;padding:7px 10px;border:1px solid #cfd7e1;border-radius:7px;color:#23303e;background:#fff;text-align:left}.version-manager-trigger:disabled{cursor:not-allowed;opacity:.72}.version-manager-trigger span{display:grid;gap:2px;min-width:0}.version-manager-trigger strong,.version-manager-trigger small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.version-manager-trigger strong{font-size:13px}.version-manager-trigger small{color:#657181;font-size:11px;font-weight:800}.version-manager-dialog{width:min(900px,100%);max-height:calc(100vh - 40px);overflow:auto;border-radius:8px;border:1px solid #d7dde4;background:#fff;box-shadow:0 18px 60px #12192338}.version-manager-body{display:grid;gap:12px;padding:16px 20px 20px;background:#f7f9fb}.version-message{padding:9px 11px;border:1px solid #cbe5df;border-radius:7px;color:#176b4f;background:#eef9f5;font-size:13px;font-weight:800}.version-message.error{border-color:#f1c1bd;color:#9a2d27;background:#fff4f3}.version-item{display:grid;gap:12px;padding:13px;border:1px solid #dfe5eb;border-radius:8px;background:#fff}.version-item.active{border-color:#7ab6ad;box-shadow:inset 3px 0 #1b6b65}.version-item-main{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:start}.version-item-title{display:flex;align-items:center;gap:8px;min-width:0}.version-item-title strong{overflow:hidden;color:#202a36;text-overflow:ellipsis;white-space:nowrap}.version-item-title span{padding:3px 7px;border-radius:999px;color:#176b4f;background:#e5f6ef;font-size:11px;font-weight:900}.version-item-main p{margin:4px 0 0;overflow-wrap:anywhere;color:#657181;font-family:SFMono-Regular,Consolas,monospace;font-size:12px}.version-item-stats{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px}.version-item-stats span,.version-env-summary span{padding:5px 7px;border-radius:6px;color:#4d5b6c;background:#eef2f6;font-size:12px;font-weight:800}.version-env-summary{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:8px}.version-env-summary code{overflow:hidden;padding:7px 9px;border:1px solid #dfe5eb;border-radius:6px;color:#2f3d4d;background:#fbfcfd;text-overflow:ellipsis;white-space:nowrap}.version-env-editor{display:grid;grid-template-columns:minmax(160px,.5fr) minmax(0,1fr);gap:10px}.version-env-editor label{display:grid;gap:5px;min-width:0;color:#647182;font-size:12px;font-weight:800}.version-env-editor input{width:100%;min-height:36px;padding:0 10px;border:1px solid #cfd7e1;border-radius:6px;outline:0;color:#15202c;background:#fff}.version-item-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end}.sync-meta{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;color:#5c6877;font-size:13px}.sync-meta span{padding:5px 8px;border:1px solid #dce2e9;border-radius:6px;background:#f7f9fb}.primary-button,.copy-inline-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;border:0;border-radius:6px;color:#fff;background:#1b6b65;font-weight:700;white-space:nowrap}.primary-button{padding:0 14px}.primary-button:disabled{cursor:wait;opacity:.72}.copy-inline-button{padding:0 12px;background:#202a36}.secondary-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:1px solid #ccd5df;border-radius:6px;color:#22303d;background:#fff;font-weight:700;white-space:nowrap}.secondary-button:disabled{cursor:wait;opacity:.72}.danger-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:1px solid #efcbc8;border-radius:6px;color:#b63831;background:#fff7f6;font-weight:700;white-space:nowrap}.icon-secondary-button{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;padding:0;border:1px solid #ccd5df;border-radius:6px;color:#22303d;background:#fff}.source-panel{display:grid;gap:10px;margin-top:12px;padding:12px;border:1px solid #d7dde4;border-radius:8px;background:#fff}.source-panel-header{display:flex;align-items:center;justify-content:space-between;gap:10px}.source-panel-actions{display:inline-flex;flex-wrap:wrap;gap:8px;justify-content:flex-end}.document-toolbar{grid-template-columns:minmax(0,1fr) auto;align-items:center}.document-source-summary{display:grid;min-width:0;gap:3px}.document-source-summary>span{color:#6a7685;font-size:12px;font-weight:700}.document-source-summary strong,.document-source-summary small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.document-source-summary strong{color:#1d2936;font-size:13px}.document-source-summary small{color:#176b4f;font-size:12px}.document-toolbar>.version-message{grid-column:1 / -1}.source-tabs{display:inline-flex;width:fit-content;gap:4px;padding:3px;border:1px solid #d4dce5;border-radius:7px;background:#fff}.source-tabs button{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:32px;padding:0 12px;border:0;border-radius:5px;color:#566373;background:transparent;font-size:13px;font-weight:800}.source-tabs button.active{color:#fff;background:#1b6b65}.source-form{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:10px}.document-dialog{width:min(760px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid #d7dde4;border-radius:8px;background:#fff;box-shadow:0 18px 60px #12192338;animation:dialog-enter .16s ease-out}.document-dialog-tabs{display:flex;gap:2px;overflow-x:auto;padding:10px 20px 0;border-bottom:1px solid #e5eaf0}.document-dialog-tabs button{display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;gap:6px;min-height:38px;padding:0 12px;border:0;border-bottom:2px solid transparent;color:#5c6877;background:transparent;font-size:13px;font-weight:800}.document-dialog-tabs button.active{border-bottom-color:#1b6b65;color:#155a55}.document-dialog-body{min-height:174px;padding:20px;background:#f7f9fb}.blank-document-fields{display:grid;gap:12px}.document-source-input{min-height:46px;background:#fff}.document-file-picker{display:grid;grid-template-columns:auto minmax(0,1fr);align-content:center;min-height:134px;padding:22px;background:#fff}.document-file-picker>svg{grid-row:1 / span 2}.document-file-picker small{color:#748091}.document-dialog-actions{gap:8px;padding-top:18px}.document-dialog-actions .clear-document-source{margin-right:auto}.source-input,.source-file-picker{display:flex;align-items:center;gap:8px;min-width:0;min-height:38px;padding:0 12px;border:1px solid #cfd7e1;border-radius:7px;background:#f9fafb}.source-file-picker{cursor:pointer}.source-file-picker input{display:none}.source-file-picker span{overflow:hidden;min-width:0;color:#15202c;text-overflow:ellipsis;white-space:nowrap}.source-input input{width:100%;min-width:0;border:0;outline:0;background:transparent;color:#15202c}.source-input textarea{width:100%;min-width:0;min-height:118px;resize:vertical;border:0;outline:0;background:transparent;color:#15202c;font-family:SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.5}.source-input-multiline{align-items:flex-start;padding-top:10px;padding-bottom:10px}.source-input-multiline svg{margin-top:2px}.source-auth{grid-column:1 / -1;display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:10px}.source-auth-fields{display:grid;grid-template-columns:repeat(2,minmax(0,220px));gap:8px}.source-auth-fields label{display:flex;align-items:center;gap:8px;min-width:0;min-height:36px;padding:0 10px;border:1px solid #cfd7e1;border-radius:7px;background:#f9fafb}.source-auth-fields input{width:100%;min-width:0;border:0;outline:0;background:transparent;color:#15202c}.source-meta{grid-column:1 / -1;overflow:hidden;color:#657181;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.source-meta span{color:#2f3d4d;font-family:SFMono-Regular,Consolas,monospace}.source-meta small{margin-left:8px;color:#728094;font-weight:800}.workspace{display:grid;flex:1 1 auto;grid-template-columns:minmax(320px,380px) minmax(0,1fr);gap:16px;min-height:0;margin-top:16px}.sidebar,.detail-pane{min-height:0;border:1px solid #d7dde4;border-radius:8px;background:#fff}.sidebar{display:flex;flex-direction:column;padding:14px}.search-box{display:flex;align-items:center;gap:8px;min-height:42px;padding:0 12px;border:1px solid #cfd7e1;border-radius:7px;background:#f9fafb}.search-box input{width:100%;min-width:0;border:0;outline:0;background:transparent;color:#15202c}.search-clear-button{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:24px;height:24px;padding:0;border:0;border-radius:5px;color:#647182;background:transparent}.search-clear-button:hover{color:#b63831;background:#fff0ef}.filters{display:grid;grid-template-columns:1fr auto;gap:10px;margin-top:12px}.filters label:first-child{display:flex;align-items:center;gap:8px;min-width:0;padding:0 10px;border:1px solid #d5dce5;border-radius:7px;background:#fff}.filters select{width:100%;min-height:38px;border:0;outline:0;background:transparent}.checkbox-line{display:inline-flex;align-items:center;gap:6px;color:#566373;font-size:13px;white-space:nowrap}.method-tabs{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:6px;margin-top:12px}.method-tabs button{min-height:32px;border:1px solid #d5dce5;border-radius:6px;color:#536171;background:#fff;font-size:12px;font-weight:800}.method-tabs button.active{color:#fff;border-color:#1b6b65;background:#1b6b65}.endpoint-count{margin:12px 2px 8px;color:#6b7582;font-size:13px}.endpoint-list{flex:1 1 auto;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:6px;padding-right:4px}.endpoint-item{display:grid;grid-template-columns:64px minmax(0,1fr);align-items:start;gap:10px;width:100%;min-height:66px;padding:10px;border:1px solid transparent;border-radius:7px;text-align:left;background:#fff}.endpoint-item:hover{background:#f6f8fa}.endpoint-item.selected{border-color:#1b6b65;background:#edf7f5}.endpoint-main{min-width:0}.endpoint-main strong,.endpoint-main small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.endpoint-main strong{margin-bottom:4px;color:#1d2732;font-size:14px}.endpoint-main small{color:#647182;font-family:SFMono-Regular,Consolas,monospace;font-size:12px}.method{display:inline-flex;align-items:center;justify-content:center;width:58px;height:24px;border-radius:5px;color:#fff;font-size:11px;font-weight:900}.method-get{background:#246bce}.method-post{background:#168456}.method-put{background:#9a6200}.method-delete{background:#c23131}.method-patch{background:#7953b8}.method-options,.method-head{background:#52606f}.detail-pane{overflow:auto}.opened-api-tabs{position:sticky;top:0;z-index:5;display:flex;align-items:flex-end;gap:4px;overflow-x:auto;overflow-y:hidden;min-height:47px;padding:10px 12px 0;border-bottom:1px solid #dfe5eb;background:#f7f9fb}.opened-api-tab{--tab-accent: #52606f;--tab-soft: #eef2f6;display:inline-grid;grid-template-columns:auto minmax(100px,210px) 20px;align-items:center;gap:7px;min-width:180px;max-width:280px;height:36px;padding:7px 8px 8px;border:1px solid color-mix(in srgb,var(--tab-accent) 45%,#d5dce5);border-bottom-color:#dfe5eb;border-radius:7px 7px 0 0;text-align:left;background:var(--tab-soft);cursor:pointer}.opened-api-tab-get{--tab-accent: #246bce;--tab-soft: #eef5ff}.opened-api-tab-post{--tab-accent: #168456;--tab-soft: #edf8f2}.opened-api-tab-put{--tab-accent: #9a6200;--tab-soft: #fff5e4}.opened-api-tab-delete{--tab-accent: #c23131;--tab-soft: #fff0f0}.opened-api-tab-patch{--tab-accent: #7953b8;--tab-soft: #f4efff}.opened-api-tab-options,.opened-api-tab-head{--tab-accent: #52606f;--tab-soft: #f0f3f6}.opened-api-tab.active{position:relative;margin-bottom:-1px;border-color:var(--tab-accent);border-bottom-color:#fff;background:#fff;box-shadow:inset 0 3px 0 var(--tab-accent)}.opened-api-tab-text{min-width:0}.opened-api-tab-text strong{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.opened-api-tab-text strong{color:#1d2732;font-size:12px;font-weight:800}.opened-api-tab-method{display:inline-flex;align-items:center;justify-content:center;min-width:34px;height:18px;padding:0 5px;border-radius:4px;color:#fff;font-size:9px;font-weight:900;line-height:1}.opened-api-tab-close{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:0;border-radius:5px;color:var(--tab-accent);background:transparent;cursor:pointer}.opened-api-tab-close:hover{color:#b63831;background:#fff0ef}.endpoint-detail{padding:22px}.detail-header{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding-bottom:18px;border-bottom:1px solid #e1e6ec}.detail-actions{display:flex;align-items:center;gap:8px;margin-left:auto}.endpoint-title-line{display:flex;align-items:center;gap:10px}.endpoint-title-line h2{margin:0;color:#121923;font-size:22px;line-height:1.25}.path-row{display:flex;align-items:center;gap:8px;min-width:0;margin-top:10px}.path-line{display:block;min-width:0;color:#354253;font-size:13px;white-space:normal;word-break:break-all}.path-copy-button{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;gap:4px;min-height:28px;padding:0 8px;border:1px solid #d4dce5;border-radius:6px;color:#415166;background:#fff;font-size:12px;font-weight:800}.path-copy-button:hover{border-color:#9fb0c1;background:#f7f9fb}.tag-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}.tag-row span{padding:4px 8px;border-radius:5px;color:#475364;background:#edf1f5;font-size:12px;font-weight:700}.description{margin:16px 0 0;color:#4f5c6a;line-height:1.7}.copy-preview{margin-top:18px;border:1px solid #d9e0e7;border-radius:8px;background:#f8fafb}.copy-preview-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px;border-bottom:1px solid #dfe5eb}.copy-tabs{display:inline-flex;gap:4px;padding:3px;border:1px solid #d4dce5;border-radius:7px;background:#fff}.copy-tabs button{display:inline-flex;align-items:center;gap:6px;min-height:30px;padding:0 10px;border:0;border-radius:5px;color:#566373;background:transparent;font-size:13px;font-weight:800}.copy-tabs button.active{color:#fff;background:#1b6b65}.copy-preview-content{overflow:auto;min-height:calc(3 * 1.6em + 28px);max-height:calc(25.6em + 28px);margin:0;padding:14px;color:#e5edf6;background:#111827;font-family:SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere}.request-tester{display:grid;gap:14px;padding:14px;background:#fff}.endpoint-links-config{display:grid;gap:12px;padding:14px;background:#fff}.links-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.links-actions,.link-editor-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end}.link-editor-panel{min-width:0}.link-button{text-decoration:none}.request-line{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:end;gap:10px}.request-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end}.request-line label,.tester-form-grid label,.param-editor label{display:grid;gap:5px;min-width:0;color:#647182;font-size:12px;font-weight:800}.request-line input,.tester-form-grid input,.tester-form-grid select,.param-editor input{width:100%;min-height:36px;padding:0 10px;border:1px solid #cfd7e1;border-radius:6px;outline:0;color:#15202c;background:#fff}.request-preview{display:block;overflow:hidden;padding:9px 10px;border:1px solid #dfe5eb;border-radius:6px;color:#334254;background:#f7f9fb;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.tester-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.tester-panel{min-width:0;padding:12px;border:1px solid #dfe5eb;border-radius:7px;background:#fbfcfd}.tester-panel-title{display:flex;align-items:center;gap:6px;margin-bottom:10px;color:#243242;font-size:13px;font-weight:900}.tester-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.param-editor+.param-editor{margin-top:12px}.param-editor h4{margin:0 0 8px;color:#516070;font-size:12px}.param-editor label+label{margin-top:8px}.param-editor span{display:inline-flex;align-items:center;gap:3px}.param-editor b{color:#c23131}.tester-panel textarea{width:100%;min-height:168px;resize:vertical;padding:10px;border:1px solid #cfd7e1;border-radius:6px;outline:0;color:#1f2b38;background:#fff;font-family:SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.5}.tester-panel textarea:disabled{color:#8792a1;background:#f2f5f8}.response-panel{background:#fff}.response-meta-line{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px}.response-meta-line span{padding:5px 8px;border-radius:5px;color:#536171;background:#eef2f6;font-size:12px;font-weight:800}.response-meta-line .status-ok{color:#176b4f;background:#e5f6ef}.response-meta-line .status-error{color:#a8322b;background:#fff0ef}.response-panel pre{overflow:auto;max-height:360px;margin:0;padding:12px;border-radius:6px;color:#202a36;background:#f7f9fb;font-family:SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.55}.detail-section{padding:20px 0;border-bottom:1px solid #e8edf2}.section-title{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.section-title h3{margin:0;font-size:16px}.section-title span{min-width:28px;padding:3px 8px;border-radius:999px;color:#1b6b65;background:#e7f4f2;text-align:center;font-size:12px;font-weight:800}.media-block+.media-block,.response-block+.response-block{margin-top:18px}.media-block h4{margin:0 0 8px;color:#516070;font-family:SFMono-Regular,Consolas,monospace;font-size:13px}.response-heading{display:flex;align-items:center;gap:10px;margin-bottom:10px}.response-heading strong{padding:4px 8px;border-radius:5px;color:#fff;background:#202a36;font-size:12px}.response-heading span{color:#657181;font-size:13px}.table-wrap{overflow:auto;border:1px solid #dfe5eb;border-radius:7px}table{width:100%;min-width:920px;border-collapse:collapse;font-size:13px}th,td{padding:10px 12px;border-bottom:1px solid #edf1f4;text-align:left;vertical-align:top}th{position:sticky;top:0;z-index:1;color:#526171;background:#f7f9fb;font-size:12px;font-weight:800}tr:last-child td{border-bottom:0}td code{color:#17202b;font-family:SFMono-Regular,Consolas,monospace;font-size:12px}.muted{margin:0;color:#798596}.inline-error{display:flex;align-items:center;gap:8px;margin-top:12px;padding:10px 12px;border:1px solid #f1c1bd;border-radius:7px;color:#9a2d27;background:#fff4f3}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:30;display:grid;place-items:center;padding:20px;background:#1219237a;animation:backdrop-enter .14s ease-out}.import-dialog{width:min(720px,100%);max-height:calc(100vh - 40px);overflow:auto;border-radius:8px;border:1px solid #d7dde4;background:#fff;box-shadow:0 18px 60px #12192338}.auth-dialog{width:min(560px,100%);max-height:calc(100vh - 40px);overflow:auto;border-radius:8px;border:1px solid #d7dde4;background:#fff;box-shadow:0 18px 60px #12192338}.cache-settings-dialog{width:min(680px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid #d7dde4;border-radius:8px;background:#fff;box-shadow:0 18px 60px #12192338}.cache-settings-body{display:grid;gap:16px;padding:18px 20px}.cache-location-row,.cache-settings-details>div{display:grid;grid-template-columns:150px minmax(0,1fr);align-items:start;gap:14px}.cache-location-row>span,.cache-settings-details dt{color:#657181;font-size:12px;font-weight:800}.cache-location-row code,.cache-settings-details dd{min-width:0;margin:0;overflow-wrap:anywhere;color:#1d2a36;font-size:12px}.cache-settings-field{display:grid;gap:7px;color:#334152;font-size:12px;font-weight:800}.cache-settings-field input{width:100%;min-height:40px;padding:0 11px;border:1px solid #bdc8d4;border-radius:6px;outline:0;color:#15202c;background:#fff}.cache-settings-field input:focus{border-color:#1b6b65;box-shadow:0 0 0 3px #1b6b651f}.cache-settings-notice{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;padding:12px 0;border-block:1px solid #dbe7e4;color:#285c55}.cache-settings-notice.warning{border-color:#ead9ad;color:#765b16}.cache-settings-notice strong{font-size:13px}.cache-settings-notice p{margin:4px 0 0;color:#657181;font-size:12px;line-height:1.55}.cache-settings-details{display:grid;gap:9px;margin:0}.cache-settings-actions{justify-content:space-between}.cache-settings-actions>div{display:flex;gap:8px}.api-config-dialog{width:80vw;min-width:1000px;max-width:80vw;max-height:calc(100vh - 40px);overflow:auto;border-radius:8px;border:1px solid #d7dde4;background:#fff;box-shadow:0 18px 60px #12192338}.import-dialog-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:18px 20px 14px;border-bottom:1px solid #e5eaf0}.import-dialog-header h2{margin:0;font-size:20px}.import-badge{display:inline-flex;align-items:center;min-height:28px;padding:0 10px;border-radius:999px;font-size:12px;font-weight:800;white-space:nowrap}.import-badge.running{color:#8a5b00;background:#fff4d8}.import-badge.success{color:#176b4f;background:#e5f6ef}.import-badge.error{color:#a8322b;background:#fff0ef}.progress-track{height:8px;margin:16px 20px 0;overflow:hidden;border-radius:999px;background:#e9eef3}.progress-bar{height:100%;border-radius:inherit;background:#1b6b65;transition:width .24s ease}.progress-bar.running{background:linear-gradient(90deg,#1b6b65,#4f8fcb)}.progress-bar.success{background:#168456}.progress-bar.error{background:#c23131}.import-summary{display:grid;gap:12px;padding:18px 20px}.auth-dialog-body{display:grid;gap:14px;padding:18px 20px}.api-config-body{display:grid;gap:14px;padding:18px 20px;background:#f7f9fb}.api-config-tabs{display:inline-flex;width:fit-content;gap:4px;padding:3px;border:1px solid #d4dce5;border-radius:7px;background:#fff}.api-config-tabs button{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:32px;padding:0 12px;border:0;border-radius:5px;color:#566373;background:transparent;font-size:13px;font-weight:800}.api-config-tabs button.active{color:#fff;background:#1b6b65}.api-config-errors{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px solid #efcbc8;border-radius:7px;color:#8f2f29;background:#fff7f6}.api-config-errors strong{display:block;margin-bottom:4px;font-size:13px}.api-config-errors p{margin:0;font-size:12px;line-height:1.45}.api-cli-editor{width:100%;min-height:calc(4.65em + 24px);max-height:calc(24.8em + 24px);overflow-y:auto;resize:vertical;padding:12px;border:1px solid #334155;border-radius:7px;outline:0;color:#e5edf6;background:#111827;font-family:SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.55}.api-cli-editor::placeholder{color:#94a3b8}.api-cli-editor:focus{border-color:#5aa69d;box-shadow:0 0 0 3px #5aa69d38}.api-config-section{display:grid;gap:12px;padding:14px;border:1px solid #dfe5eb;border-radius:8px;background:#fff}.section-title-row{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.section-title-row h3{margin:0;color:#243242;font-size:15px}.section-title-row p{margin:4px 0 0;color:#657181;font-size:12px}.section-title-row code{max-width:420px;overflow:hidden;padding:5px 7px;border:1px solid #dce2e9;border-radius:6px;color:#415166;background:#f7f9fb;text-overflow:ellipsis;white-space:nowrap}.api-config-grid{display:grid;grid-template-columns:140px repeat(2,minmax(0,1fr));gap:10px}.api-config-grid.compact{grid-template-columns:repeat(3,minmax(0,1fr)) auto}.api-config-grid label,.field-config-row label{display:grid;gap:5px;min-width:0;color:#647182;font-size:12px;font-weight:800}.api-config-grid input,.api-config-grid select,.api-config-grid textarea,.field-config-row input,.field-config-row select{width:100%;min-height:36px;padding:0 10px;border:1px solid #cfd7e1;border-radius:6px;outline:0;color:#15202c;background:#fff}.api-config-grid textarea{min-height:82px;padding:10px;resize:vertical;line-height:1.45}.span-2{grid-column:span 2}.span-all{grid-column:1 / -1}.inline-check{display:inline-flex;align-items:center;gap:6px;min-height:32px;color:#415166;font-size:13px;font-weight:800}.field-config-table{display:grid;gap:8px;overflow-x:auto}.field-config-item{display:grid;gap:8px}.field-config-head,.field-config-row{display:grid;grid-template-columns:minmax(150px,1.1fr) 120px 58px minmax(180px,1.2fr) minmax(120px,.8fr) 38px;gap:8px;align-items:center;min-width:760px}.field-config-head.with-location,.field-config-row.with-location{grid-template-columns:98px minmax(150px,1.1fr) 120px 58px minmax(180px,1.2fr) minmax(120px,.8fr) 38px}.field-config-head{color:#657181;font-size:12px;font-weight:900}.field-check{place-items:center}.field-check input,.inline-check input{width:16px;min-height:16px}.icon-danger-button{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border:1px solid #efcbc8;border-radius:6px;color:#b63831;background:#fff7f6}.field-add-row{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:34px;border:1px dashed #b8c4d0;border-radius:6px;color:#415166;background:#fbfcfd;font-weight:800}.child-field-config{display:grid;gap:8px;margin-left:18px;padding:10px;border-left:3px solid #b9d7d3;border-radius:0 7px 7px 0;background:#f7fbfa}.child-field-title{color:#415166;font-size:12px;font-weight:900}.response-config{display:grid;gap:10px;padding:12px;border:1px solid #e3e8ee;border-radius:7px;background:#fbfcfd}.auth-message{display:flex;align-items:flex-start;gap:8px;padding:10px 12px;border:1px solid #d9e0e7;border-radius:7px;color:#2f3d4d;background:#f7f9fb}.auth-target{display:grid;gap:4px}.auth-target span,.auth-fields label{color:#657181;font-size:12px;font-weight:800}.auth-target strong{overflow-wrap:anywhere;color:#1f2b38;font-family:SFMono-Regular,Consolas,monospace;font-size:13px;line-height:1.5}.auth-fields{display:grid;gap:10px}.auth-fields label{display:grid;gap:6px}.auth-fields input{width:100%;min-height:38px;padding:0 10px;border:1px solid #cfd7e1;border-radius:6px;outline:0;color:#15202c;background:#fff}.import-summary div:not(.import-counts):not(.import-error){display:grid;gap:4px}.import-summary span{color:#657181;font-size:12px;font-weight:700}.import-summary strong{overflow-wrap:anywhere;color:#1f2b38;font-family:SFMono-Regular,Consolas,monospace;font-size:13px;line-height:1.5}.import-counts{display:flex;flex-wrap:wrap;gap:8px}.import-counts span{padding:6px 9px;border-radius:6px;color:#1b6b65;background:#e7f4f2}.import-error{display:flex;align-items:flex-start;gap:8px;padding:10px 12px;border:1px solid #f1c1bd;border-radius:7px;color:#9a2d27;background:#fff4f3}.import-dialog-actions{display:flex;justify-content:flex-end;padding:0 20px 18px}.state-screen,.empty-state{display:grid;place-items:center;align-content:center;min-height:100vh;padding:24px;text-align:center}.empty-state{min-height:60vh}.state-icon{width:42px;height:42px;margin-bottom:14px;color:#1b6b65}.state-icon.error{color:#b8312d}.state-screen h1,.empty-state h2{margin-bottom:8px}.spin{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}@keyframes backdrop-enter{0%{opacity:0}}@keyframes dialog-enter{0%{opacity:0;transform:translateY(8px)}}@media(max-width:980px){.app-shell{height:auto;padding:12px;overflow:visible}.topbar{align-items:flex-start;flex-direction:column}.topbar-actions{width:100%;justify-content:space-between}.version-switcher,.version-switcher select{width:100%}.version-manager{width:100%;min-width:0}.version-item-main,.version-env-editor,.version-env-summary{grid-template-columns:1fr}.version-item-stats,.version-item-actions{justify-content:flex-start}.source-panel{grid-template-columns:1fr}.source-panel-header{align-items:stretch;flex-direction:column}.source-panel-header>.secondary-button,.source-panel-actions,.source-tabs{width:100%}.source-panel-actions{justify-content:stretch}.source-panel-actions .secondary-button{flex:1 1 180px}.source-tabs{flex-wrap:wrap}.document-dialog-tabs{width:100%}.source-form,.source-auth,.source-auth-fields,.api-config-grid,.api-config-grid.compact{grid-template-columns:1fr}.span-2,.span-all{grid-column:auto}.workspace{grid-template-columns:1fr;flex:none;height:auto}.sidebar{max-height:46vh}.detail-pane{min-height:60vh}.detail-header{flex-direction:column}.copy-preview-toolbar{align-items:stretch;flex-direction:column}.copy-tabs,.copy-inline-button{width:100%}.copy-tabs button{flex:1;justify-content:center}.request-line,.tester-grid,.links-grid,.tester-form-grid{grid-template-columns:1fr}}@media(max-width:640px){.filters{grid-template-columns:1fr}.method-tabs{grid-template-columns:repeat(3,minmax(0,1fr))}.topbar-actions,.sync-meta{align-items:stretch;flex-direction:column;width:100%}.primary-button,.secondary-button,.copy-inline-button,.source-tabs{width:100%}.source-tabs button{flex:1}.document-dialog{max-height:calc(100vh - 20px)}.document-dialog-tabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));overflow:visible;padding-inline:12px}.document-dialog-tabs button{min-width:0;padding-inline:6px}.document-dialog-body{min-height:210px;padding:16px}.document-dialog-actions .clear-document-source{margin-right:0}.import-dialog-actions .secondary-button{width:100%}.import-dialog-actions{flex-direction:column-reverse;gap:8px}.cache-location-row,.cache-settings-details>div{grid-template-columns:1fr;gap:5px}.cache-settings-actions,.cache-settings-actions>div{display:grid;width:100%}.endpoint-title-line{align-items:flex-start;flex-direction:column}}
|
package/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>API Skill Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-B_wAJLu_.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CHOQ6xhP.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/docs/cli.md
CHANGED
|
@@ -35,6 +35,19 @@ You can also run the executable directly:
|
|
|
35
35
|
node scripts/apiskill-cli.mjs --help
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## Cache Settings
|
|
39
|
+
|
|
40
|
+
Web, CLI, and MCP share the saved default in `~/.apiskill/config.json`:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
apiskill cache show
|
|
44
|
+
apiskill cache show --json
|
|
45
|
+
apiskill cache set /absolute/path/to/apiskill-cache
|
|
46
|
+
apiskill cache reset
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Changing the default never moves or deletes existing files. Future CLI and MCP processes use the new location immediately; restart Web or use Update Cache Location in version management to copy selected versions. `APISKILL_CACHE_DIR` has priority over the saved setting.
|
|
50
|
+
|
|
38
51
|
## Import Documents
|
|
39
52
|
|
|
40
53
|
Check whether a usable cache is available:
|
package/docs/cli.zh.md
CHANGED
|
@@ -35,6 +35,19 @@ npm run cli -- --help
|
|
|
35
35
|
node scripts/apiskill-cli.mjs --help
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## 缓存设置
|
|
39
|
+
|
|
40
|
+
Web、CLI 和 MCP 共用保存在 `~/.apiskill/config.json` 中的默认缓存地址:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
apiskill cache show
|
|
44
|
+
apiskill cache show --json
|
|
45
|
+
apiskill cache set /absolute/path/to/apiskill-cache
|
|
46
|
+
apiskill cache reset
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
修改默认地址不会移动或删除已有缓存文件。后续 CLI、MCP 进程会直接使用新地址,Web 需要重启;也可以在 Web 版本管理中选择“更新缓存地址”,将需要的版本复制到新目录。`APISKILL_CACHE_DIR` 环境变量优先于保存的默认设置。
|
|
50
|
+
|
|
38
51
|
## 导入文档
|
|
39
52
|
|
|
40
53
|
先检查当前是否有可用缓存:
|
package/docs/web.md
CHANGED
|
@@ -20,6 +20,8 @@ npm run dev
|
|
|
20
20
|
|
|
21
21
|
Open the local URL printed by the terminal.
|
|
22
22
|
|
|
23
|
+
Cache Settings in the upper-right corner shows the active session location and changes the saved default. Existing files are never moved or deleted. Restart Web to use the new location; Update Cache Location on a version shows the source and destination before copying while retaining the source file.
|
|
24
|
+
|
|
23
25
|
## Import Sources
|
|
24
26
|
|
|
25
27
|
The web app supports four import paths:
|
package/docs/web.zh.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apiskill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"lucide-react": "^0.468.0",
|
|
35
35
|
"react": "^18.3.1",
|
|
36
36
|
"react-dom": "^18.3.1",
|
|
37
|
+
"typescript": "^5.8.3",
|
|
37
38
|
"vite": "^6.0.7",
|
|
38
39
|
"yaml": "^2.8.3"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/node": "^22.10.2",
|
|
42
43
|
"@types/react": "^18.3.18",
|
|
43
|
-
"@types/react-dom": "^18.3.5"
|
|
44
|
-
"typescript": "^7.0.2"
|
|
44
|
+
"@types/react-dom": "^18.3.5"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/scripts/apiskill-cli.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { crawlOpenApi, importFromCurl, importFromLocalFile, importFromUrl } from './lib/openapi-importer.mjs';
|
|
20
20
|
import { checkCache, formatCheckText, queryEndpoints } from './lib/apiskill-core.mjs';
|
|
21
21
|
import { startMockServer } from './lib/mock-server.mjs';
|
|
22
|
-
import { getDefaultCacheDir } from './lib/cache-paths.mjs';
|
|
22
|
+
import { getCacheSettings, getDefaultCacheDir, resetDefaultCacheDir, setDefaultCacheDir } from './lib/cache-paths.mjs';
|
|
23
23
|
|
|
24
24
|
const program = new Command();
|
|
25
25
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -41,6 +41,8 @@ Examples:
|
|
|
41
41
|
apiskill import-file ./openapi.json
|
|
42
42
|
apiskill import-curl --file ./request.curl
|
|
43
43
|
apiskill run web
|
|
44
|
+
apiskill cache show
|
|
45
|
+
apiskill cache set ~/.apiskill/cache
|
|
44
46
|
apiskill mock
|
|
45
47
|
apiskill document create --title "My API" --doc-version 1.0.0
|
|
46
48
|
apiskill check
|
|
@@ -59,6 +61,36 @@ CLI config format:
|
|
|
59
61
|
`,
|
|
60
62
|
);
|
|
61
63
|
|
|
64
|
+
const cache = program.command('cache').description('View or change the shared default document cache directory.');
|
|
65
|
+
|
|
66
|
+
cache
|
|
67
|
+
.command('show')
|
|
68
|
+
.description('Show the active and saved default cache directories.')
|
|
69
|
+
.option('-j, --json', 'Print JSON')
|
|
70
|
+
.action((options) => {
|
|
71
|
+
const settings = getCacheSettings();
|
|
72
|
+
print(options.json ? settings : formatCacheSettingsText(settings));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
cache
|
|
76
|
+
.command('set')
|
|
77
|
+
.description('Save the default cache directory used by future CLI, MCP, and Web processes. Existing files are not moved.')
|
|
78
|
+
.argument('<directory>', 'Absolute path or a path beginning with ~/')
|
|
79
|
+
.option('-j, --json', 'Print JSON')
|
|
80
|
+
.action(async (directory, options) => {
|
|
81
|
+
const settings = await setDefaultCacheDir(directory);
|
|
82
|
+
print(options.json ? settings : `${formatCacheSettingsText(settings)}\nExisting cached files were not moved.`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
cache
|
|
86
|
+
.command('reset')
|
|
87
|
+
.description('Restore the built-in ~/.apiskill/cache default. Existing files are not moved.')
|
|
88
|
+
.option('-j, --json', 'Print JSON')
|
|
89
|
+
.action(async (options) => {
|
|
90
|
+
const settings = await resetDefaultCacheDir();
|
|
91
|
+
print(options.json ? settings : `${formatCacheSettingsText(settings)}\nExisting cached files were not moved.`);
|
|
92
|
+
});
|
|
93
|
+
|
|
62
94
|
const run = program.command('run').description('Run API Skill local services.');
|
|
63
95
|
|
|
64
96
|
run
|
|
@@ -301,7 +333,6 @@ async function runWeb(options) {
|
|
|
301
333
|
const host = options.host || '127.0.0.1';
|
|
302
334
|
|
|
303
335
|
process.env.APISKILL_ROOT = projectRoot;
|
|
304
|
-
process.env.APISKILL_CACHE_DIR = cacheDir;
|
|
305
336
|
|
|
306
337
|
const { createServer } = await import('vite');
|
|
307
338
|
const server = await createServer({
|
|
@@ -374,3 +405,16 @@ function print(value) {
|
|
|
374
405
|
if (typeof value === 'string') console.log(value);
|
|
375
406
|
else console.log(JSON.stringify(value, null, 2));
|
|
376
407
|
}
|
|
408
|
+
|
|
409
|
+
function formatCacheSettingsText(settings) {
|
|
410
|
+
return [
|
|
411
|
+
`Active cache: ${settings.cacheDir}`,
|
|
412
|
+
`Saved default: ${settings.defaultCacheDir}`,
|
|
413
|
+
`Built-in default: ${settings.builtInCacheDir}`,
|
|
414
|
+
`Source: ${settings.source}`,
|
|
415
|
+
settings.environmentOverride ? `Environment override: ${settings.environmentCacheDir}` : '',
|
|
416
|
+
`Config: ${settings.configPath}`,
|
|
417
|
+
]
|
|
418
|
+
.filter(Boolean)
|
|
419
|
+
.join('\n');
|
|
420
|
+
}
|
|
@@ -1,7 +1,98 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const CONFIG_FILE_NAME = 'config.json';
|
|
7
|
+
|
|
8
|
+
export function getCacheSettings(env = process.env) {
|
|
9
|
+
const appDataDir = getAppDataDir(env);
|
|
10
|
+
const configPath = resolve(appDataDir, CONFIG_FILE_NAME);
|
|
11
|
+
const builtInCacheDir = resolve(homedir(), '.apiskill', 'cache');
|
|
12
|
+
const configuredCacheDir = readConfiguredCacheDir(configPath);
|
|
13
|
+
const defaultCacheDir = configuredCacheDir || builtInCacheDir;
|
|
14
|
+
const environmentCacheDir = env.APISKILL_CACHE_DIR ? normalizeCacheDir(env.APISKILL_CACHE_DIR) : '';
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
cacheDir: environmentCacheDir || defaultCacheDir,
|
|
18
|
+
defaultCacheDir,
|
|
19
|
+
configuredCacheDir,
|
|
20
|
+
builtInCacheDir,
|
|
21
|
+
environmentCacheDir,
|
|
22
|
+
environmentOverride: Boolean(environmentCacheDir),
|
|
23
|
+
source: environmentCacheDir ? 'environment' : configuredCacheDir ? 'config' : 'default',
|
|
24
|
+
configPath,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
3
27
|
|
|
4
28
|
export function getDefaultCacheDir(env = process.env) {
|
|
5
|
-
|
|
6
|
-
|
|
29
|
+
return getCacheSettings(env).cacheDir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function setDefaultCacheDir(value, env = process.env) {
|
|
33
|
+
const cacheDir = normalizeCacheDir(value);
|
|
34
|
+
const settings = getCacheSettings(env);
|
|
35
|
+
await mkdir(cacheDir, { recursive: true });
|
|
36
|
+
const writeProbePath = resolve(cacheDir, `.apiskill-write-test-${process.pid}-${Date.now()}`);
|
|
37
|
+
try {
|
|
38
|
+
await writeFile(writeProbePath, 'ok');
|
|
39
|
+
} finally {
|
|
40
|
+
await rm(writeProbePath, { force: true }).catch(() => {});
|
|
41
|
+
}
|
|
42
|
+
await mkdir(dirname(settings.configPath), { recursive: true });
|
|
43
|
+
|
|
44
|
+
const currentConfig = readConfig(settings.configPath);
|
|
45
|
+
const nextConfig = {
|
|
46
|
+
...currentConfig,
|
|
47
|
+
cacheDir,
|
|
48
|
+
cacheDirUpdatedAt: new Date().toISOString(),
|
|
49
|
+
};
|
|
50
|
+
const temporaryPath = `${settings.configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
51
|
+
await writeFile(temporaryPath, JSON.stringify(nextConfig, null, 2));
|
|
52
|
+
await rename(temporaryPath, settings.configPath);
|
|
53
|
+
return getCacheSettings(env);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function resetDefaultCacheDir(env = process.env) {
|
|
57
|
+
const settings = getCacheSettings(env);
|
|
58
|
+
const currentConfig = readConfig(settings.configPath);
|
|
59
|
+
delete currentConfig.cacheDir;
|
|
60
|
+
currentConfig.cacheDirUpdatedAt = new Date().toISOString();
|
|
61
|
+
await mkdir(dirname(settings.configPath), { recursive: true });
|
|
62
|
+
const temporaryPath = `${settings.configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
63
|
+
await writeFile(temporaryPath, JSON.stringify(currentConfig, null, 2));
|
|
64
|
+
await rename(temporaryPath, settings.configPath);
|
|
65
|
+
return getCacheSettings(env);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function normalizeCacheDir(value) {
|
|
69
|
+
const input = String(value || '').trim();
|
|
70
|
+
if (!input) throw new Error('缓存目录不能为空');
|
|
71
|
+
if (input === '~') return homedir();
|
|
72
|
+
if (input.startsWith('~/')) return resolve(homedir(), input.slice(2));
|
|
73
|
+
return resolve(input);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getAppDataDir(env) {
|
|
77
|
+
return env.APISKILL_CONFIG_DIR ? normalizeCacheDir(env.APISKILL_CONFIG_DIR) : resolve(homedir(), '.apiskill');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readConfiguredCacheDir(configPath) {
|
|
81
|
+
const value = readConfig(configPath).cacheDir;
|
|
82
|
+
if (typeof value !== 'string' || !value.trim()) return '';
|
|
83
|
+
try {
|
|
84
|
+
return normalizeCacheDir(value);
|
|
85
|
+
} catch {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readConfig(configPath) {
|
|
91
|
+
if (!existsSync(configPath)) return {};
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
94
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
95
|
+
} catch {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
7
98
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { createServer } from 'node:net';
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
4
5
|
import { homedir, tmpdir } from 'node:os';
|
|
5
6
|
import { dirname, join, resolve } from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -9,11 +10,15 @@ import { getDefaultCacheDir } from './lib/cache-paths.mjs';
|
|
|
9
10
|
|
|
10
11
|
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
12
|
const cliPath = resolve(projectRoot, 'scripts/apiskill-cli.mjs');
|
|
12
|
-
const
|
|
13
|
+
const testRoot = await mkdtemp(join(tmpdir(), 'apiskill-shared-cache-'));
|
|
14
|
+
const cacheDir = resolve(testRoot, 'active-cache');
|
|
15
|
+
const defaultCacheDir = resolve(testRoot, 'default-cache');
|
|
16
|
+
const configDir = resolve(testRoot, 'config');
|
|
13
17
|
const port = await findFreePort();
|
|
14
18
|
let webProcess;
|
|
15
19
|
|
|
16
20
|
try {
|
|
21
|
+
await mkdir(cacheDir, { recursive: true });
|
|
17
22
|
assert.equal(getDefaultCacheDir({ APISKILL_ROOT: '/tmp/another-project' }), resolve(homedir(), '.apiskill', 'cache'));
|
|
18
23
|
|
|
19
24
|
const created = JSON.parse(
|
|
@@ -42,7 +47,7 @@ try {
|
|
|
42
47
|
[cliPath, 'run', 'web', '--host', '127.0.0.1', '--port', String(port), '--strict-port', '--cwd', projectRoot],
|
|
43
48
|
{
|
|
44
49
|
cwd: projectRoot,
|
|
45
|
-
env: { ...process.env, APISKILL_CACHE_DIR: cacheDir },
|
|
50
|
+
env: { ...process.env, APISKILL_CACHE_DIR: cacheDir, APISKILL_CONFIG_DIR: configDir },
|
|
46
51
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
52
|
},
|
|
48
53
|
);
|
|
@@ -76,20 +81,50 @@ try {
|
|
|
76
81
|
|
|
77
82
|
const check = JSON.parse(await runCli(['check', '--json']));
|
|
78
83
|
assert.equal(check.cacheDir, cacheDir);
|
|
79
|
-
|
|
84
|
+
|
|
85
|
+
const settingsResponse = await fetch(`http://127.0.0.1:${port}/api/cache-settings`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'content-type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({ cacheDir: defaultCacheDir }),
|
|
89
|
+
});
|
|
90
|
+
const settings = await settingsResponse.json();
|
|
91
|
+
assert.equal(settingsResponse.status, 200, JSON.stringify(settings));
|
|
92
|
+
assert.equal(settings.activeCacheDir, cacheDir);
|
|
93
|
+
assert.equal(settings.defaultCacheDir, defaultCacheDir);
|
|
94
|
+
assert.equal(settings.environmentOverride, true);
|
|
95
|
+
|
|
96
|
+
const syncResponse = await fetch(`http://127.0.0.1:${port}/api/openapi/version-sync-cache`, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'content-type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ versionId }),
|
|
100
|
+
});
|
|
101
|
+
const synced = await syncResponse.json();
|
|
102
|
+
assert.equal(syncResponse.status, 200, JSON.stringify(synced));
|
|
103
|
+
assert.equal(synced.targetCacheDir, defaultCacheDir);
|
|
104
|
+
assert.ok(existsSync(resolve(cacheDir, 'versions', `${versionId}.json`)), 'Sync removed the source document');
|
|
105
|
+
assert.ok(existsSync(resolve(defaultCacheDir, 'versions', `${versionId}.json`)), 'Sync did not create the target document');
|
|
106
|
+
|
|
107
|
+
const cliFromNewDefault = JSON.parse(
|
|
108
|
+
await runCli(['api', 'query', 'POST', '/api/v1/from-web'], { cacheOverride: false }),
|
|
109
|
+
);
|
|
110
|
+
assert.equal(cliFromNewDefault.path, '/api/v1/from-web');
|
|
111
|
+
console.log(`Shared cache test passed: Web and CLI synchronized ${versionId} from ${cacheDir} to ${defaultCacheDir}.`);
|
|
80
112
|
} finally {
|
|
81
113
|
if (webProcess && webProcess.exitCode === null) {
|
|
82
114
|
webProcess.kill('SIGTERM');
|
|
83
115
|
await new Promise((resolvePromise) => webProcess.once('close', resolvePromise));
|
|
84
116
|
}
|
|
85
|
-
await rm(
|
|
117
|
+
await rm(testRoot, { recursive: true, force: true });
|
|
86
118
|
}
|
|
87
119
|
|
|
88
|
-
function runCli(args) {
|
|
120
|
+
function runCli(args, { cacheOverride = true } = {}) {
|
|
89
121
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
122
|
+
const env = { ...process.env, APISKILL_CONFIG_DIR: configDir };
|
|
123
|
+
if (cacheOverride) env.APISKILL_CACHE_DIR = cacheDir;
|
|
124
|
+
else delete env.APISKILL_CACHE_DIR;
|
|
90
125
|
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
91
126
|
cwd: projectRoot,
|
|
92
|
-
env
|
|
127
|
+
env,
|
|
93
128
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
94
129
|
});
|
|
95
130
|
const output = collectProcessOutput(child);
|
package/src/App.tsx
CHANGED
|
@@ -44,9 +44,12 @@ import {
|
|
|
44
44
|
VERSION_DELETE_API_URL,
|
|
45
45
|
VERSION_META_API_URL,
|
|
46
46
|
VERSION_SELECT_API_URL,
|
|
47
|
+
VERSION_SYNC_CACHE_API_URL,
|
|
47
48
|
VERSIONS_API_URL,
|
|
48
49
|
} from './swagger';
|
|
49
50
|
import { AddApiDialog } from './AddApiDialog';
|
|
51
|
+
import { CacheSettingsDialog, loadCacheSettings } from './CacheSettingsDialog';
|
|
52
|
+
import type { CacheSettings } from './CacheSettingsDialog';
|
|
50
53
|
import { DocumentVersionManager } from './DocumentVersionManager';
|
|
51
54
|
import type { DocumentVersionMeta } from './DocumentVersionManager';
|
|
52
55
|
import type { ManualApiOperationConfig } from './manualApiConfig';
|
|
@@ -125,6 +128,16 @@ type VersionDeleteResponse = {
|
|
|
125
128
|
nextVersionId?: string;
|
|
126
129
|
};
|
|
127
130
|
|
|
131
|
+
type VersionCacheSyncResponse = {
|
|
132
|
+
status: 'synced' | 'already-current';
|
|
133
|
+
versionId: string;
|
|
134
|
+
sourceCacheDir: string;
|
|
135
|
+
sourceDocumentPath: string;
|
|
136
|
+
targetCacheDir: string;
|
|
137
|
+
targetDocumentPath: string;
|
|
138
|
+
syncedAt?: string;
|
|
139
|
+
};
|
|
140
|
+
|
|
128
141
|
type AuthCheckResponse = {
|
|
129
142
|
requiresAuth: boolean;
|
|
130
143
|
status?: number;
|
|
@@ -220,6 +233,7 @@ export function App() {
|
|
|
220
233
|
const [editingApiTarget, setEditingApiTarget] = useState<{ method: HttpMethod; path: string } | undefined>(undefined);
|
|
221
234
|
const [operationLinkDrafts, setOperationLinkDrafts] = useState<Record<string, EndpointAssociationLinks>>({});
|
|
222
235
|
const [savingApi, setSavingApi] = useState(false);
|
|
236
|
+
const [cacheSettings, setCacheSettings] = useState<CacheSettings | undefined>(undefined);
|
|
223
237
|
const uploadInputRef = useRef<HTMLInputElement | null>(null);
|
|
224
238
|
|
|
225
239
|
useEffect(() => {
|
|
@@ -236,6 +250,7 @@ export function App() {
|
|
|
236
250
|
|
|
237
251
|
loadInitialCache();
|
|
238
252
|
void loadMockStatus();
|
|
253
|
+
void loadCacheSettings().then(setCacheSettings).catch(() => {});
|
|
239
254
|
}, []);
|
|
240
255
|
|
|
241
256
|
const endpoints = useMemo(() => (doc ? listEndpoints(doc) : []), [doc]);
|
|
@@ -756,6 +771,17 @@ export function App() {
|
|
|
756
771
|
setLoadState('ready');
|
|
757
772
|
}
|
|
758
773
|
|
|
774
|
+
async function syncVersionToDefaultCache(versionId: string) {
|
|
775
|
+
const response = await fetch(VERSION_SYNC_CACHE_API_URL, {
|
|
776
|
+
method: 'POST',
|
|
777
|
+
headers: { 'content-type': 'application/json' },
|
|
778
|
+
body: JSON.stringify({ versionId }),
|
|
779
|
+
});
|
|
780
|
+
const payload = await response.json();
|
|
781
|
+
if (!response.ok) throw new Error(payload.error || '文档同步到默认缓存地址失败');
|
|
782
|
+
return payload as VersionCacheSyncResponse;
|
|
783
|
+
}
|
|
784
|
+
|
|
759
785
|
async function loadMockStatus() {
|
|
760
786
|
try {
|
|
761
787
|
const response = await fetch(MOCK_STATUS_API_URL, { cache: 'no-store' });
|
|
@@ -940,6 +966,8 @@ export function App() {
|
|
|
940
966
|
onPrepareUpdate={prepareVersionUpdate}
|
|
941
967
|
onUpdateEnvironment={updateVersionEnvironment}
|
|
942
968
|
onDelete={deleteVersion}
|
|
969
|
+
cacheSettings={cacheSettings}
|
|
970
|
+
onSyncCache={syncVersionToDefaultCache}
|
|
943
971
|
/>
|
|
944
972
|
<button
|
|
945
973
|
className="primary-button"
|
|
@@ -957,6 +985,7 @@ export function App() {
|
|
|
957
985
|
<span>Schema {stats.schemas}</span>
|
|
958
986
|
<span>{lastSync ? `同步 ${formatDate(lastSync)}` : '未加载文档'}</span>
|
|
959
987
|
</div>
|
|
988
|
+
<CacheSettingsDialog settings={cacheSettings} onSettingsChange={setCacheSettings} />
|
|
960
989
|
</div>
|
|
961
990
|
</header>
|
|
962
991
|
|