apiskill 0.1.4 → 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.
@@ -0,0 +1,175 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createServer } from 'node:net';
3
+ import { existsSync } from 'node:fs';
4
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
5
+ import { homedir, tmpdir } from 'node:os';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { spawn } from 'node:child_process';
9
+ import { getDefaultCacheDir } from './lib/cache-paths.mjs';
10
+
11
+ const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
12
+ const cliPath = resolve(projectRoot, 'scripts/apiskill-cli.mjs');
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');
17
+ const port = await findFreePort();
18
+ let webProcess;
19
+
20
+ try {
21
+ await mkdir(cacheDir, { recursive: true });
22
+ assert.equal(getDefaultCacheDir({ APISKILL_ROOT: '/tmp/another-project' }), resolve(homedir(), '.apiskill', 'cache'));
23
+
24
+ const created = JSON.parse(
25
+ await runCli(['document', 'create', '--title', 'Shared Cache Test', '--doc-version', '1.0.0', '--json']),
26
+ );
27
+ const versionId = created.meta.versionId;
28
+ await runCli([
29
+ 'api',
30
+ 'create',
31
+ '--version',
32
+ versionId,
33
+ '--config',
34
+ JSON.stringify({
35
+ api: {
36
+ method: 'get',
37
+ path: '/api/v1/from-cli',
38
+ summary: 'Created from CLI',
39
+ responses: [{ status: '200', description: 'Success' }],
40
+ },
41
+ }),
42
+ '--json',
43
+ ]);
44
+
45
+ webProcess = spawn(
46
+ process.execPath,
47
+ [cliPath, 'run', 'web', '--host', '127.0.0.1', '--port', String(port), '--strict-port', '--cwd', projectRoot],
48
+ {
49
+ cwd: projectRoot,
50
+ env: { ...process.env, APISKILL_CACHE_DIR: cacheDir, APISKILL_CONFIG_DIR: configDir },
51
+ stdio: ['ignore', 'pipe', 'pipe'],
52
+ },
53
+ );
54
+ const webOutput = collectProcessOutput(webProcess);
55
+ await waitForWeb(port, webProcess, webOutput);
56
+
57
+ const webCacheResponse = await fetch(`http://127.0.0.1:${port}/api/openapi/cache`);
58
+ assert.equal(webCacheResponse.status, 200);
59
+ const webCache = await webCacheResponse.json();
60
+ assert.equal(webCache.meta.versionId, versionId);
61
+ assert.ok(webCache.document.paths['/api/v1/from-cli']?.get, 'Web did not read the API created by CLI');
62
+
63
+ const webWriteResponse = await fetch(`http://127.0.0.1:${port}/api/openapi/custom-operation`, {
64
+ method: 'POST',
65
+ headers: { 'content-type': 'application/json' },
66
+ body: JSON.stringify({
67
+ versionId,
68
+ config: {
69
+ method: 'post',
70
+ path: '/api/v1/from-web',
71
+ summary: 'Created from Web',
72
+ responses: [{ status: '201', description: 'Created' }],
73
+ },
74
+ }),
75
+ });
76
+ assert.equal(webWriteResponse.status, 200, await webWriteResponse.text());
77
+
78
+ const cliRead = JSON.parse(await runCli(['api', 'query', 'POST', '/api/v1/from-web', '--version', versionId]));
79
+ assert.equal(cliRead.path, '/api/v1/from-web');
80
+ assert.equal(cliRead.method, 'post');
81
+
82
+ const check = JSON.parse(await runCli(['check', '--json']));
83
+ assert.equal(check.cacheDir, cacheDir);
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}.`);
112
+ } finally {
113
+ if (webProcess && webProcess.exitCode === null) {
114
+ webProcess.kill('SIGTERM');
115
+ await new Promise((resolvePromise) => webProcess.once('close', resolvePromise));
116
+ }
117
+ await rm(testRoot, { recursive: true, force: true });
118
+ }
119
+
120
+ function runCli(args, { cacheOverride = true } = {}) {
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;
125
+ const child = spawn(process.execPath, [cliPath, ...args], {
126
+ cwd: projectRoot,
127
+ env,
128
+ stdio: ['ignore', 'pipe', 'pipe'],
129
+ });
130
+ const output = collectProcessOutput(child);
131
+ child.on('error', rejectPromise);
132
+ child.on('close', (code) => {
133
+ if (code === 0) resolvePromise(output.stdout);
134
+ else rejectPromise(new Error(`apiskill ${args.join(' ')} failed (${code}): ${output.stderr || output.stdout}`));
135
+ });
136
+ });
137
+ }
138
+
139
+ function collectProcessOutput(child) {
140
+ const output = { stdout: '', stderr: '' };
141
+ child.stdout.setEncoding('utf8');
142
+ child.stderr.setEncoding('utf8');
143
+ child.stdout.on('data', (chunk) => {
144
+ output.stdout += chunk;
145
+ });
146
+ child.stderr.on('data', (chunk) => {
147
+ output.stderr += chunk;
148
+ });
149
+ return output;
150
+ }
151
+
152
+ async function waitForWeb(webPort, child, output) {
153
+ const deadline = Date.now() + 15_000;
154
+ while (Date.now() < deadline) {
155
+ if (child.exitCode !== null) throw new Error(`Web process exited early (${child.exitCode}): ${output.stderr || output.stdout}`);
156
+ try {
157
+ const response = await fetch(`http://127.0.0.1:${webPort}/api/openapi/versions`);
158
+ if (response.ok) return;
159
+ } catch {}
160
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
161
+ }
162
+ throw new Error(`Timed out waiting for Web: ${output.stderr || output.stdout}`);
163
+ }
164
+
165
+ function findFreePort() {
166
+ return new Promise((resolvePromise, rejectPromise) => {
167
+ const server = createServer();
168
+ server.once('error', rejectPromise);
169
+ server.listen(0, '127.0.0.1', () => {
170
+ const address = server.address();
171
+ const freePort = typeof address === 'object' && address ? address.port : 0;
172
+ server.close((error) => (error ? rejectPromise(error) : resolvePromise(freePort)));
173
+ });
174
+ });
175
+ }
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
 
@@ -0,0 +1,166 @@
1
+ import { useState } from 'react';
2
+ import { AlertCircle, FolderCog, RotateCcw, Save, X } from 'lucide-react';
3
+ import { CACHE_SETTINGS_API_URL } from './swagger';
4
+
5
+ export type CacheSettings = {
6
+ activeCacheDir: string;
7
+ defaultCacheDir: string;
8
+ configuredCacheDir: string;
9
+ builtInCacheDir: string;
10
+ environmentCacheDir: string;
11
+ environmentOverride: boolean;
12
+ effectiveNextCacheDir: string;
13
+ source: 'environment' | 'config' | 'default';
14
+ configPath: string;
15
+ restartRequired: boolean;
16
+ activeMatchesDefault: boolean;
17
+ };
18
+
19
+ export function CacheSettingsDialog({
20
+ settings,
21
+ onSettingsChange,
22
+ }: {
23
+ settings?: CacheSettings;
24
+ onSettingsChange: (settings: CacheSettings) => void;
25
+ }) {
26
+ const [open, setOpen] = useState(false);
27
+ const [cacheDir, setCacheDir] = useState('');
28
+ const [loading, setLoading] = useState(false);
29
+ const [message, setMessage] = useState('');
30
+ const [error, setError] = useState('');
31
+
32
+ async function openDialog() {
33
+ setOpen(true);
34
+ setMessage('');
35
+ setError('');
36
+ setLoading(true);
37
+ try {
38
+ const next = await loadCacheSettings();
39
+ onSettingsChange(next);
40
+ setCacheDir(next.defaultCacheDir);
41
+ } catch (loadError) {
42
+ setError(loadError instanceof Error ? loadError.message : '缓存设置读取失败');
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }
47
+
48
+ async function saveSettings(reset = false) {
49
+ setLoading(true);
50
+ setMessage('');
51
+ setError('');
52
+ try {
53
+ const response = await fetch(CACHE_SETTINGS_API_URL, {
54
+ method: 'POST',
55
+ headers: { 'content-type': 'application/json' },
56
+ body: JSON.stringify(reset ? { reset: true } : { cacheDir }),
57
+ });
58
+ const payload = await response.json();
59
+ if (!response.ok) throw new Error(payload.error || '缓存设置保存失败');
60
+ const next = payload as CacheSettings;
61
+ onSettingsChange(next);
62
+ setCacheDir(next.defaultCacheDir);
63
+ setMessage(reset ? '已恢复内置默认地址,现有缓存文件未改动' : '默认缓存地址已保存,现有缓存文件未改动');
64
+ } catch (saveError) {
65
+ setError(saveError instanceof Error ? saveError.message : '缓存设置保存失败');
66
+ } finally {
67
+ setLoading(false);
68
+ }
69
+ }
70
+
71
+ return (
72
+ <>
73
+ <button className="secondary-button cache-settings-trigger" onClick={openDialog} title="设置接口文档默认缓存地址">
74
+ <FolderCog size={16} />
75
+ 缓存设置
76
+ </button>
77
+
78
+ {open ? (
79
+ <div className="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="cache-settings-title">
80
+ <div className="cache-settings-dialog">
81
+ <div className="import-dialog-header">
82
+ <div>
83
+ <p className="eyebrow">Cache Settings</p>
84
+ <h2 id="cache-settings-title">接口文档缓存设置</h2>
85
+ </div>
86
+ <button className="icon-secondary-button" onClick={() => setOpen(false)} aria-label="关闭">
87
+ <X size={17} />
88
+ </button>
89
+ </div>
90
+
91
+ <div className="cache-settings-body">
92
+ {error ? <div className="version-message error">{error}</div> : null}
93
+ {message ? <div className="version-message">{message}</div> : null}
94
+
95
+ <div className="cache-location-row">
96
+ <span>当前会话缓存位置</span>
97
+ <code>{settings?.activeCacheDir || (loading ? '读取中...' : '-')}</code>
98
+ </div>
99
+
100
+ <label className="cache-settings-field">
101
+ 默认缓存地址
102
+ <input
103
+ value={cacheDir}
104
+ onChange={(event) => setCacheDir(event.target.value)}
105
+ placeholder="例如 ~/.apiskill/cache"
106
+ autoComplete="off"
107
+ disabled={loading}
108
+ />
109
+ </label>
110
+
111
+ <div className="cache-settings-notice">
112
+ <AlertCircle size={18} />
113
+ <div>
114
+ <strong>修改地址不会移动或删除已有缓存文件</strong>
115
+ <p>新的 CLI、MCP 进程会读取该默认地址;Web 端重启后切换。需要保留的版本可在版本管理中使用“更新缓存地址”复制过去。</p>
116
+ </div>
117
+ </div>
118
+
119
+ {settings?.environmentOverride ? (
120
+ <div className="cache-settings-notice warning">
121
+ <AlertCircle size={18} />
122
+ <div>
123
+ <strong>环境变量正在覆盖默认设置</strong>
124
+ <p>当前 `APISKILL_CACHE_DIR` 为 {settings.environmentCacheDir}。只要该变量存在,重启后仍优先使用它。</p>
125
+ </div>
126
+ </div>
127
+ ) : null}
128
+
129
+ <dl className="cache-settings-details">
130
+ <div>
131
+ <dt>内置默认地址</dt>
132
+ <dd>{settings?.builtInCacheDir || '-'}</dd>
133
+ </div>
134
+ <div>
135
+ <dt>配置文件</dt>
136
+ <dd>{settings?.configPath || '-'}</dd>
137
+ </div>
138
+ </dl>
139
+ </div>
140
+
141
+ <div className="import-dialog-actions cache-settings-actions">
142
+ <button className="secondary-button" onClick={() => void saveSettings(true)} disabled={loading}>
143
+ <RotateCcw size={16} />
144
+ 恢复默认
145
+ </button>
146
+ <div>
147
+ <button className="secondary-button" onClick={() => setOpen(false)} disabled={loading}>取消</button>
148
+ <button className="primary-button" onClick={() => void saveSettings(false)} disabled={loading || !cacheDir.trim()}>
149
+ <Save size={16} />
150
+ {loading ? '保存中' : '保存设置'}
151
+ </button>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ ) : null}
157
+ </>
158
+ );
159
+ }
160
+
161
+ export async function loadCacheSettings(): Promise<CacheSettings> {
162
+ const response = await fetch(CACHE_SETTINGS_API_URL, { cache: 'no-store' });
163
+ const payload = await response.json();
164
+ if (!response.ok) throw new Error(payload.error || '缓存设置读取失败');
165
+ return payload as CacheSettings;
166
+ }
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useMemo, useState } from 'react';
2
- import { Check, Download, Pencil, RefreshCcw, Server, Trash2, X } from 'lucide-react';
2
+ import { Check, Download, FolderSync, Pencil, RefreshCcw, Server, Trash2, X } from 'lucide-react';
3
+ import type { CacheSettings } from './CacheSettingsDialog';
3
4
  import { VERSION_EXPORT_API_URL } from './swagger';
4
5
 
5
6
  export type DocumentVersionMeta = {
@@ -14,6 +15,7 @@ export type DocumentVersionMeta = {
14
15
  schemas?: number;
15
16
  environmentName?: string;
16
17
  environmentBaseUrl?: string;
18
+ savedPath?: string;
17
19
  };
18
20
 
19
21
  export function DocumentVersionManager({
@@ -25,6 +27,8 @@ export function DocumentVersionManager({
25
27
  onPrepareUpdate,
26
28
  onUpdateEnvironment,
27
29
  onDelete,
30
+ cacheSettings,
31
+ onSyncCache,
28
32
  }: {
29
33
  versions: DocumentVersionMeta[];
30
34
  selectedVersionId: string;
@@ -34,6 +38,12 @@ export function DocumentVersionManager({
34
38
  onPrepareUpdate: (version: DocumentVersionMeta) => Promise<void> | void;
35
39
  onUpdateEnvironment: (versionId: string, environmentName: string, environmentBaseUrl: string) => Promise<void>;
36
40
  onDelete: (versionId: string) => Promise<void>;
41
+ cacheSettings?: CacheSettings;
42
+ onSyncCache: (versionId: string) => Promise<{
43
+ status: 'synced' | 'already-current';
44
+ targetCacheDir: string;
45
+ targetDocumentPath: string;
46
+ }>;
37
47
  }) {
38
48
  const [open, setOpen] = useState(false);
39
49
  const [editingVersionId, setEditingVersionId] = useState('');
@@ -118,6 +128,38 @@ export function DocumentVersionManager({
118
128
  document.body.removeChild(anchor);
119
129
  }
120
130
 
131
+ async function syncVersionCache(version: DocumentVersionMeta) {
132
+ const versionId = version.versionId;
133
+ if (!versionId) return;
134
+ if (!cacheSettings) {
135
+ setMessage('缓存设置尚未读取,请稍后重试');
136
+ return;
137
+ }
138
+
139
+ const sourceLocation = version.savedPath || cacheSettings.activeCacheDir;
140
+ const targetLocation = cacheSettings.defaultCacheDir;
141
+ if (cacheSettings.activeMatchesDefault) {
142
+ window.alert(`当前缓存位置:\n${sourceLocation}\n\n该版本已位于默认缓存地址:\n${targetLocation}`);
143
+ return;
144
+ }
145
+
146
+ const confirmed = window.confirm(
147
+ `当前缓存位置:\n${sourceLocation}\n\n默认缓存地址:\n${targetLocation}\n\n确认将该文档版本复制到默认缓存地址吗?源文件不会被移动或删除;目标中同版本内容将被更新。`,
148
+ );
149
+ if (!confirmed) return;
150
+
151
+ setBusyVersionId(versionId);
152
+ setMessage('');
153
+ try {
154
+ const result = await onSyncCache(versionId);
155
+ setMessage(result.status === 'already-current' ? '该版本已位于默认缓存地址' : `已同步到 ${result.targetDocumentPath}`);
156
+ } catch (error) {
157
+ setMessage(error instanceof Error ? error.message : '更新缓存地址失败');
158
+ } finally {
159
+ setBusyVersionId('');
160
+ }
161
+ }
162
+
121
163
  return (
122
164
  <div className="version-manager">
123
165
  <button className="version-manager-trigger" onClick={() => setOpen(true)} disabled={disabled || !versions.length}>
@@ -231,6 +273,10 @@ export function DocumentVersionManager({
231
273
  <Download size={15} />
232
274
  导出
233
275
  </button>
276
+ <button className="secondary-button" onClick={() => void syncVersionCache(version)} disabled={busy || !versionId}>
277
+ <FolderSync size={15} />
278
+ 更新缓存地址
279
+ </button>
234
280
  <button className="danger-button" onClick={() => deleteVersion(version)} disabled={busy || !versionId}>
235
281
  <Trash2 size={15} />
236
282
  删除
package/src/styles.css CHANGED
@@ -75,7 +75,10 @@ p {
75
75
  .topbar-actions {
76
76
  display: flex;
77
77
  align-items: center;
78
+ justify-content: flex-end;
79
+ flex-wrap: wrap;
78
80
  gap: 14px;
81
+ min-width: 0;
79
82
  }
80
83
 
81
84
  .version-switcher {
@@ -1542,6 +1545,110 @@ td code {
1542
1545
  box-shadow: 0 18px 60px rgba(18, 25, 35, 0.22);
1543
1546
  }
1544
1547
 
1548
+ .cache-settings-dialog {
1549
+ width: min(680px, 100%);
1550
+ max-height: calc(100vh - 40px);
1551
+ overflow: auto;
1552
+ border: 1px solid #d7dde4;
1553
+ border-radius: 8px;
1554
+ background: #fff;
1555
+ box-shadow: 0 18px 60px rgba(18, 25, 35, 0.22);
1556
+ }
1557
+
1558
+ .cache-settings-body {
1559
+ display: grid;
1560
+ gap: 16px;
1561
+ padding: 18px 20px;
1562
+ }
1563
+
1564
+ .cache-location-row,
1565
+ .cache-settings-details > div {
1566
+ display: grid;
1567
+ grid-template-columns: 150px minmax(0, 1fr);
1568
+ align-items: start;
1569
+ gap: 14px;
1570
+ }
1571
+
1572
+ .cache-location-row > span,
1573
+ .cache-settings-details dt {
1574
+ color: #657181;
1575
+ font-size: 12px;
1576
+ font-weight: 800;
1577
+ }
1578
+
1579
+ .cache-location-row code,
1580
+ .cache-settings-details dd {
1581
+ min-width: 0;
1582
+ margin: 0;
1583
+ overflow-wrap: anywhere;
1584
+ color: #1d2a36;
1585
+ font-size: 12px;
1586
+ }
1587
+
1588
+ .cache-settings-field {
1589
+ display: grid;
1590
+ gap: 7px;
1591
+ color: #334152;
1592
+ font-size: 12px;
1593
+ font-weight: 800;
1594
+ }
1595
+
1596
+ .cache-settings-field input {
1597
+ width: 100%;
1598
+ min-height: 40px;
1599
+ padding: 0 11px;
1600
+ border: 1px solid #bdc8d4;
1601
+ border-radius: 6px;
1602
+ outline: 0;
1603
+ color: #15202c;
1604
+ background: #fff;
1605
+ }
1606
+
1607
+ .cache-settings-field input:focus {
1608
+ border-color: #1b6b65;
1609
+ box-shadow: 0 0 0 3px rgba(27, 107, 101, 0.12);
1610
+ }
1611
+
1612
+ .cache-settings-notice {
1613
+ display: grid;
1614
+ grid-template-columns: auto minmax(0, 1fr);
1615
+ gap: 10px;
1616
+ padding: 12px 0;
1617
+ border-block: 1px solid #dbe7e4;
1618
+ color: #285c55;
1619
+ }
1620
+
1621
+ .cache-settings-notice.warning {
1622
+ border-color: #ead9ad;
1623
+ color: #765b16;
1624
+ }
1625
+
1626
+ .cache-settings-notice strong {
1627
+ font-size: 13px;
1628
+ }
1629
+
1630
+ .cache-settings-notice p {
1631
+ margin: 4px 0 0;
1632
+ color: #657181;
1633
+ font-size: 12px;
1634
+ line-height: 1.55;
1635
+ }
1636
+
1637
+ .cache-settings-details {
1638
+ display: grid;
1639
+ gap: 9px;
1640
+ margin: 0;
1641
+ }
1642
+
1643
+ .cache-settings-actions {
1644
+ justify-content: space-between;
1645
+ }
1646
+
1647
+ .cache-settings-actions > div {
1648
+ display: flex;
1649
+ gap: 8px;
1650
+ }
1651
+
1545
1652
  .api-config-dialog {
1546
1653
  width: 80vw;
1547
1654
  min-width: 1000px;
@@ -2260,6 +2367,18 @@ td code {
2260
2367
  gap: 8px;
2261
2368
  }
2262
2369
 
2370
+ .cache-location-row,
2371
+ .cache-settings-details > div {
2372
+ grid-template-columns: 1fr;
2373
+ gap: 5px;
2374
+ }
2375
+
2376
+ .cache-settings-actions,
2377
+ .cache-settings-actions > div {
2378
+ display: grid;
2379
+ width: 100%;
2380
+ }
2381
+
2263
2382
  .endpoint-title-line {
2264
2383
  align-items: flex-start;
2265
2384
  flex-direction: column;
package/src/swagger.ts CHANGED
@@ -19,6 +19,8 @@ export const VERSION_SELECT_API_URL = '/api/openapi/version-select';
19
19
  export const VERSION_META_API_URL = '/api/openapi/version-meta';
20
20
  export const VERSION_DELETE_API_URL = '/api/openapi/version-delete';
21
21
  export const VERSION_EXPORT_API_URL = '/api/openapi/version-export';
22
+ export const VERSION_SYNC_CACHE_API_URL = '/api/openapi/version-sync-cache';
23
+ export const CACHE_SETTINGS_API_URL = '/api/cache-settings';
22
24
  export const MOCK_START_API_URL = '/api/mock/start';
23
25
  export const MOCK_STATUS_API_URL = '/api/mock/status';
24
26
  export const MOCK_STOP_API_URL = '/api/mock/stop';