@aflueza/dev-tools 0.0.0-stage → 0.2.1-rc.2
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 +12 -2
- package/cordis.patch.yml +3 -0
- package/lib/api-runner/database.js +216 -0
- package/lib/api-runner/service.js +121 -0
- package/lib/client.js +180001 -0
- package/lib/crypto/service.js +242 -0
- package/lib/index.js +41 -0
- package/lib/mock-data/database.js +92 -0
- package/lib/process-manager/rpc.js +277 -0
- package/lib/process-manager/service.js +708 -0
- package/lib/shared/api-runner.js +1 -0
- package/lib/shared/crypto.js +1 -0
- package/lib/shared/mock-data.js +27 -0
- package/lib/shared/process-manager.js +1 -0
- package/lib/style.css +14655 -0
- package/package.json +66 -4
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Aflueza Dev Tools DSH Plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
独立的 DSH 开发工具插件,通过右侧侧边栏提供开发辅助工具。
|
|
4
|
+
|
|
5
|
+
当前包含命名格式、JSON、XML formatter、文本对比、Hit API、Mock 数据和进程管理器。代码编辑区使用 Monaco Editor;文本对比支持实时左右对照、交换和清空文本、复制任一侧内容、忽略行首尾空白及差异统计。进程管理器通过 DSH 主进程读取本机进程和端口信息;按钮、菜单、图标和视觉 token 与 Harness 保持一致。
|
|
6
|
+
|
|
7
|
+
API 请求历史、Mock 数据和加密配置保存在 `~/.dsh/plugins/dev-tools/` 下的 SQLite 数据库中;同一 Windows 用户的 Web 与桌面端共用这些数据。
|
|
8
|
+
|
|
9
|
+
进程管理器支持按名称、命令行、PID 或端口号搜索,默认不加载全量进程,并提供详情、端口绑定、自动刷新和结束进程。也可以输入完整文件或目录路径,查找当前占用该文件、或占用目录内文件的进程;目录查询会流式递归遍历并按批检查,一旦发现占用进程就立即停止继续扫描。Windows 使用系统 Restart Manager,其他平台依赖 `lsof`。读取或结束受当前用户权限限制,失败会在面板内展示。
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
pnpm dsh plugin --profile web add @aflueza/dev-tools@0.2.1-rc.2
|
|
13
|
+
```
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
const SCHEMA_VERSION = 2;
|
|
5
|
+
const HISTORY_LIMIT = 100;
|
|
6
|
+
const json = (value) => JSON.stringify(value);
|
|
7
|
+
const parse = (value) => JSON.parse(value);
|
|
8
|
+
export class ApiDatabase {
|
|
9
|
+
historyLimit;
|
|
10
|
+
db;
|
|
11
|
+
writeQueue = Promise.resolve();
|
|
12
|
+
constructor(path, historyLimit = HISTORY_LIMIT) {
|
|
13
|
+
this.historyLimit = historyLimit;
|
|
14
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
15
|
+
this.db = new DatabaseSync(path);
|
|
16
|
+
this.db.exec('PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
|
|
17
|
+
const version = Number(this.db.prepare('PRAGMA user_version').get().user_version);
|
|
18
|
+
if (version > SCHEMA_VERSION)
|
|
19
|
+
throw new Error(`api database schema ${version} is newer than supported ${SCHEMA_VERSION}`);
|
|
20
|
+
this.db
|
|
21
|
+
.exec(`CREATE TABLE IF NOT EXISTS api_collections (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, order_index INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);
|
|
22
|
+
CREATE TABLE IF NOT EXISTS api_requests (id INTEGER PRIMARY KEY AUTOINCREMENT, collection_id INTEGER NOT NULL REFERENCES api_collections(id) ON DELETE CASCADE, name TEXT NOT NULL, order_index INTEGER NOT NULL DEFAULT 0, method TEXT NOT NULL, url TEXT NOT NULL, query_params_json TEXT NOT NULL, headers_json TEXT NOT NULL, body_mode TEXT NOT NULL, body TEXT NOT NULL, body_form_data_json TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS api_history (id INTEGER PRIMARY KEY AUTOINCREMENT, request_snapshot_json TEXT NOT NULL, response_summary_json TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);`);
|
|
24
|
+
for (const sql of [
|
|
25
|
+
'ALTER TABLE api_collections ADD COLUMN order_index INTEGER NOT NULL DEFAULT 0',
|
|
26
|
+
'ALTER TABLE api_requests ADD COLUMN order_index INTEGER NOT NULL DEFAULT 0',
|
|
27
|
+
]) {
|
|
28
|
+
try {
|
|
29
|
+
this.db.exec(sql);
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
}
|
|
33
|
+
this.db.exec('UPDATE api_collections SET order_index=id WHERE order_index=0; UPDATE api_requests SET order_index=id WHERE order_index=0;');
|
|
34
|
+
this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
35
|
+
}
|
|
36
|
+
close() {
|
|
37
|
+
this.db.close();
|
|
38
|
+
}
|
|
39
|
+
enqueue(task) {
|
|
40
|
+
const run = this.writeQueue.then(task);
|
|
41
|
+
this.writeQueue = run.then(() => undefined, () => undefined);
|
|
42
|
+
return run;
|
|
43
|
+
}
|
|
44
|
+
listCollections() {
|
|
45
|
+
return this.db
|
|
46
|
+
.prepare('SELECT id,name,order_index,created_at,updated_at FROM api_collections ORDER BY order_index,id')
|
|
47
|
+
.all().map(this.collection);
|
|
48
|
+
}
|
|
49
|
+
createCollection(name) {
|
|
50
|
+
return this.enqueue(() => {
|
|
51
|
+
const n = name.trim() || '新集合';
|
|
52
|
+
const max = Number(this.db.prepare('SELECT COALESCE(MAX(order_index),0) n FROM api_collections').get().n);
|
|
53
|
+
this.db.prepare('INSERT INTO api_collections(name,order_index) VALUES (?,?)').run(n, max + 1);
|
|
54
|
+
const row = this.db.prepare('SELECT last_insert_rowid() id').get();
|
|
55
|
+
if (row.id === undefined)
|
|
56
|
+
throw new Error('failed to read created collection id');
|
|
57
|
+
return this.getCollection(Number(row.id));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
ensureCollection(name) {
|
|
61
|
+
const found = this.listCollections().find((x) => x.name === (name.trim() || '新集合'));
|
|
62
|
+
return found ? Promise.resolve(found) : this.createCollection(name);
|
|
63
|
+
}
|
|
64
|
+
renameCollection(id, name) {
|
|
65
|
+
return this.enqueue(() => {
|
|
66
|
+
this.db
|
|
67
|
+
.prepare('UPDATE api_collections SET name=?,updated_at=CURRENT_TIMESTAMP WHERE id=?')
|
|
68
|
+
.run(name.trim() || '未命名集合', id);
|
|
69
|
+
return this.getCollection(id);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
deleteCollection(id) {
|
|
73
|
+
return this.enqueue(() => {
|
|
74
|
+
this.db.prepare('DELETE FROM api_collections WHERE id=?').run(id);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
getCollection(id) {
|
|
78
|
+
const row = this.db
|
|
79
|
+
.prepare('SELECT id,name,created_at,updated_at FROM api_collections WHERE id=?')
|
|
80
|
+
.get(id);
|
|
81
|
+
if (!row)
|
|
82
|
+
throw new Error('collection not found');
|
|
83
|
+
return this.collection(row);
|
|
84
|
+
}
|
|
85
|
+
collection(row) {
|
|
86
|
+
return {
|
|
87
|
+
id: Number(row.id),
|
|
88
|
+
name: String(row.name),
|
|
89
|
+
orderIndex: Number(row.order_index),
|
|
90
|
+
createdAt: String(row.created_at),
|
|
91
|
+
updatedAt: String(row.updated_at),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
listRequests() {
|
|
95
|
+
return this.db
|
|
96
|
+
.prepare('SELECT * FROM api_requests ORDER BY collection_id,order_index,id')
|
|
97
|
+
.all().map((row) => ({
|
|
98
|
+
id: Number(row.id),
|
|
99
|
+
collectionId: Number(row.collection_id),
|
|
100
|
+
name: String(row.name),
|
|
101
|
+
orderIndex: Number(row.order_index),
|
|
102
|
+
request: {
|
|
103
|
+
method: row.method,
|
|
104
|
+
url: row.url,
|
|
105
|
+
queryParams: parse(row.query_params_json),
|
|
106
|
+
headers: parse(row.headers_json),
|
|
107
|
+
bodyMode: row.body_mode,
|
|
108
|
+
body: row.body,
|
|
109
|
+
bodyFormData: parse(row.body_form_data_json),
|
|
110
|
+
},
|
|
111
|
+
createdAt: String(row.created_at),
|
|
112
|
+
updatedAt: String(row.updated_at),
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
saveRequest(payload) {
|
|
116
|
+
return this.enqueue(() => {
|
|
117
|
+
const r = sanitize(payload.request);
|
|
118
|
+
const args = [
|
|
119
|
+
payload.collectionId,
|
|
120
|
+
payload.name.trim() || '未命名请求',
|
|
121
|
+
r.method,
|
|
122
|
+
r.url,
|
|
123
|
+
json(r.queryParams),
|
|
124
|
+
json(r.headers),
|
|
125
|
+
r.bodyMode,
|
|
126
|
+
r.body,
|
|
127
|
+
json(r.bodyFormData),
|
|
128
|
+
];
|
|
129
|
+
let id = payload.id;
|
|
130
|
+
if (id)
|
|
131
|
+
this.db
|
|
132
|
+
.prepare('UPDATE api_requests SET collection_id=?,name=?,method=?,url=?,query_params_json=?,headers_json=?,body_mode=?,body=?,body_form_data_json=?,updated_at=CURRENT_TIMESTAMP WHERE id=?')
|
|
133
|
+
.run(...args, id);
|
|
134
|
+
else {
|
|
135
|
+
this.db
|
|
136
|
+
.prepare('INSERT INTO api_requests(collection_id,name,method,url,query_params_json,headers_json,body_mode,body,body_form_data_json) VALUES(?,?,?,?,?,?,?,?,?)')
|
|
137
|
+
.run(...args);
|
|
138
|
+
const row = this.db.prepare('SELECT last_insert_rowid() id').get();
|
|
139
|
+
if (row.id === undefined)
|
|
140
|
+
throw new Error('failed to read created request id');
|
|
141
|
+
id = Number(row.id);
|
|
142
|
+
}
|
|
143
|
+
return this.listRequests().find((x) => x.id === id);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
renameRequest(id, name) {
|
|
147
|
+
return this.enqueue(() => {
|
|
148
|
+
this.db
|
|
149
|
+
.prepare('UPDATE api_requests SET name=?,updated_at=CURRENT_TIMESTAMP WHERE id=?')
|
|
150
|
+
.run(name.trim() || '未命名请求', id);
|
|
151
|
+
return this.listRequests().find((x) => x.id === id);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
reorderCollections(ids) {
|
|
155
|
+
return this.enqueue(() => ids.forEach((id, i) => this.db.prepare('UPDATE api_collections SET order_index=? WHERE id=?').run(i, id)));
|
|
156
|
+
}
|
|
157
|
+
reorderRequests(collectionId, ids) {
|
|
158
|
+
return this.enqueue(() => ids.forEach((id, i) => this.db
|
|
159
|
+
.prepare('UPDATE api_requests SET order_index=? WHERE id=? AND collection_id=?')
|
|
160
|
+
.run(i, id, collectionId)));
|
|
161
|
+
}
|
|
162
|
+
moveRequest(id, collectionId) {
|
|
163
|
+
return this.enqueue(() => {
|
|
164
|
+
const max = Number(this.db
|
|
165
|
+
.prepare('SELECT COALESCE(MAX(order_index),0) n FROM api_requests WHERE collection_id=?')
|
|
166
|
+
.get(collectionId).n);
|
|
167
|
+
this.db
|
|
168
|
+
.prepare('UPDATE api_requests SET collection_id=?,order_index=?,updated_at=CURRENT_TIMESTAMP WHERE id=?')
|
|
169
|
+
.run(collectionId, max + 1, id);
|
|
170
|
+
return this.listRequests().find((x) => x.id === id);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
deleteRequest(id) {
|
|
174
|
+
return this.enqueue(() => {
|
|
175
|
+
this.db.prepare('DELETE FROM api_requests WHERE id=?').run(id);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
listHistory(limit = this.historyLimit) {
|
|
179
|
+
return this.db
|
|
180
|
+
.prepare('SELECT * FROM api_history ORDER BY id DESC LIMIT ?')
|
|
181
|
+
.all(Math.min(Math.max(limit, 1), this.historyLimit)).map((row) => ({
|
|
182
|
+
id: Number(row.id),
|
|
183
|
+
requestSnapshot: parse(row.request_snapshot_json),
|
|
184
|
+
responseSummary: parse(row.response_summary_json),
|
|
185
|
+
createdAt: String(row.created_at),
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
addHistory(request, response) {
|
|
189
|
+
return this.enqueue(() => {
|
|
190
|
+
this.db
|
|
191
|
+
.prepare('INSERT INTO api_history(request_snapshot_json,response_summary_json) VALUES(?,?)')
|
|
192
|
+
.run(json(sanitize(request)), json(response));
|
|
193
|
+
this.db
|
|
194
|
+
.prepare('DELETE FROM api_history WHERE id NOT IN (SELECT id FROM api_history ORDER BY id DESC LIMIT ?)')
|
|
195
|
+
.run(this.historyLimit);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
deleteHistory(id) {
|
|
199
|
+
return this.enqueue(() => {
|
|
200
|
+
this.db.prepare('DELETE FROM api_history WHERE id=?').run(id);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
clearHistory() {
|
|
204
|
+
return this.enqueue(() => {
|
|
205
|
+
this.db.prepare('DELETE FROM api_history').run();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function sanitize(r) {
|
|
210
|
+
return {
|
|
211
|
+
...r,
|
|
212
|
+
queryParams: r.queryParams ?? [],
|
|
213
|
+
headers: r.headers ?? [],
|
|
214
|
+
bodyFormData: (r.bodyFormData ?? []).map((x) => ({ ...x, fileContent: undefined })),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
import { ApiDatabase } from './database.js';
|
|
4
|
+
export class ApiRunnerError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class ApiRunnerService {
|
|
12
|
+
timeoutMs;
|
|
13
|
+
maxBytes;
|
|
14
|
+
database;
|
|
15
|
+
constructor(path, timeoutMs = 30_000, maxBytes = 4 * 1024 * 1024, historyLimit = 100) {
|
|
16
|
+
this.timeoutMs = timeoutMs;
|
|
17
|
+
this.maxBytes = maxBytes;
|
|
18
|
+
this.database = new ApiDatabase(path, historyLimit);
|
|
19
|
+
}
|
|
20
|
+
close() {
|
|
21
|
+
this.database.close();
|
|
22
|
+
}
|
|
23
|
+
async send(request) {
|
|
24
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method))
|
|
25
|
+
throw new ApiRunnerError('invalid-method', '仅支持 GET、POST、PUT、PATCH 和 DELETE 请求。');
|
|
26
|
+
let url;
|
|
27
|
+
try {
|
|
28
|
+
url = new URL(request.url.trim());
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new ApiRunnerError('invalid-url', 'URL 无效。');
|
|
32
|
+
}
|
|
33
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
34
|
+
throw new ApiRunnerError('invalid-url', 'URL 必须以 http:// 或 https:// 开头。');
|
|
35
|
+
for (const row of request.queryParams ?? [])
|
|
36
|
+
if (row.enabled && row.key.trim())
|
|
37
|
+
url.searchParams.append(row.key.trim(), row.value);
|
|
38
|
+
const headers = new Headers();
|
|
39
|
+
for (const row of request.headers ?? [])
|
|
40
|
+
if (row.enabled && row.key.trim()) {
|
|
41
|
+
try {
|
|
42
|
+
headers.set(row.key.trim(), row.value);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new ApiRunnerError('invalid-header', `请求头无效:${row.key}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let body;
|
|
49
|
+
if (request.method !== 'GET' && request.bodyMode !== 'none') {
|
|
50
|
+
if (request.bodyMode === 'form-data') {
|
|
51
|
+
const form = new FormData();
|
|
52
|
+
for (const row of request.bodyFormData ?? [])
|
|
53
|
+
if (row.enabled && row.key.trim()) {
|
|
54
|
+
if (row.kind === 'file' && (row.filePath || row.fileContent?.length)) {
|
|
55
|
+
const bytes = row.filePath
|
|
56
|
+
? await readFile(row.filePath)
|
|
57
|
+
: new Uint8Array(row.fileContent ?? []);
|
|
58
|
+
form.append(row.key, new Blob([bytes], { type: row.fileMimeType || 'application/octet-stream' }), row.fileName);
|
|
59
|
+
}
|
|
60
|
+
else
|
|
61
|
+
form.append(row.key, row.value);
|
|
62
|
+
}
|
|
63
|
+
body = form;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
body = request.body;
|
|
67
|
+
if (request.bodyMode === 'json' && !headers.has('content-type'))
|
|
68
|
+
headers.set('content-type', 'application/json');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const started = performance.now();
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch(url, {
|
|
76
|
+
method: request.method,
|
|
77
|
+
headers,
|
|
78
|
+
body,
|
|
79
|
+
signal: controller.signal,
|
|
80
|
+
});
|
|
81
|
+
const reader = response.body?.getReader();
|
|
82
|
+
let text = '';
|
|
83
|
+
let size = 0;
|
|
84
|
+
if (reader) {
|
|
85
|
+
const decoder = new TextDecoder();
|
|
86
|
+
while (true) {
|
|
87
|
+
const next = await reader.read();
|
|
88
|
+
if (next.done) {
|
|
89
|
+
text += decoder.decode();
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
size += next.value.byteLength;
|
|
93
|
+
if (size > this.maxBytes)
|
|
94
|
+
throw new ApiRunnerError('response-too-large', '响应超过大小限制。');
|
|
95
|
+
text += decoder.decode(next.value, { stream: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
text = await response.text();
|
|
100
|
+
const result = {
|
|
101
|
+
status: response.status,
|
|
102
|
+
statusText: response.statusText,
|
|
103
|
+
durationMs: Math.round(performance.now() - started),
|
|
104
|
+
body: text,
|
|
105
|
+
headers: [...response.headers].map(([key, value]) => ({ key, value })),
|
|
106
|
+
};
|
|
107
|
+
await this.database.addHistory(request, result);
|
|
108
|
+
return { ...result, request };
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (error instanceof ApiRunnerError)
|
|
112
|
+
throw error;
|
|
113
|
+
if (controller.signal.aborted)
|
|
114
|
+
throw new ApiRunnerError('timeout', '请求超时。');
|
|
115
|
+
throw new ApiRunnerError('network-error', error instanceof Error ? error.message : String(error));
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|