autowonder 0.3.5 → 0.3.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 -10
- package/bin/token-proxy.js +102 -60
- package/package.json +1 -1
- package/vendor/autowonder-daemon-darwin-amd64 +0 -0
- package/vendor/autowonder-daemon-darwin-arm64 +0 -0
- package/vendor/autowonder-daemon-linux-amd64 +0 -0
- package/vendor/autowonder-daemon-linux-arm64 +0 -0
- package/vendor/autowonder-daemon-win32-amd64.exe +0 -0
- package/vendor/autowonder-daemon-win32-arm64.exe +0 -0
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@ AutoWonder 本地 agent runtime。安装后启动 daemon,持续轮询本地 as
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# Qoder CLI(模型使用 provider model ID;Context Window 使用固定档位)
|
|
9
|
-
npx -y autowonder@0.3.
|
|
9
|
+
npx -y autowonder@0.3.6 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qoder --model qmodel_latest --reasoning-effort medium --context-window 260000
|
|
10
10
|
|
|
11
11
|
# Qoder CLI CN
|
|
12
|
-
npx -y autowonder@0.3.
|
|
12
|
+
npx -y autowonder@0.3.6 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
`connect` 会安装当前 npm 包内置的 daemon,并使用页面生成的 WebSocket endpoint、Token 和执行器 ID 建立连接。客户端只支持通过 Qoder 系列 CLI 启动(`qoder` 对应 `qodercli`,`qodercn` 对应 `qodercn`),其他 `--provider` 取值会在启动 daemon 前直接报错拦截并提示 `Unsupported provider`。选择 Qoder CLI 或 Qoder CLI CN 时需要 Node.js 20+;runtime 会在缺少对应 CLI 时自动通过 npm 安装,并在尚未登录时打开浏览器登录。runtime 会继承当前用户的 HOME、环境变量和 CLI 登录状态。
|
|
@@ -37,7 +37,7 @@ daemon 会响应服务端的 `QODER_MODEL_CATALOG_REQUEST` 帧,用当前 provi
|
|
|
37
37
|
|
|
38
38
|
daemon 还会响应服务端的 `CONVERSATION_COMMANDS_PROBE` 帧,用独立会话抓取该会话工作目录下可用的斜杠命令,回传 `CONVERSATION_COMMANDS_RESULT` 帧,让会话面板在用户输入 `/` 时列出真实可用的命令。该探测与回合执行完全隔离:任何失败只产生 `FAILED` / `TIMEOUT` 帧,不影响进行中的会话;provider 后端不支持抓取时同样降级为 `FAILED` 帧。帧里的 `executorId` 与本机不一致或非正数时直接忽略,避免在别的执行器工作区里建目录或启动 CLI。daemon 侧给整个探测 25 秒硬上限,刻意长于 agent 侧的 20 秒预算,使 agent 自己的超时先触发并可被归类。
|
|
39
39
|
|
|
40
|
-
给 `connect` 追加 `--token-aware-enable` 可开启 qoder / qodercn 的 token 用量采集:CLI 会以 sidecar 方式启动本地 HTTPS 转发代理(`bin/token-proxy.js`),从 Qoder API 的 SSE 响应流中提取 usage 数据并按 dispatch ID
|
|
40
|
+
给 `connect` 追加 `--token-aware-enable` 可开启 qoder / qodercn 的 token 用量采集:CLI 会以 sidecar 方式启动本地 HTTPS 转发代理(`bin/token-proxy.js`),从 Qoder API 的 SSE 响应流中提取 usage 数据并按 dispatch ID 归属上报。usage 解析失败只丢失计量数据;代理启动失败时仅告警并继续。启用后模型流量经过代理,连接失败由客户端处理重试,不保证自动直连。代理端口可用 `TOKEN_PROXY_PORT` 环境变量调整(默认 19876);CA 证书位于 `~/.autowonder/token-tap/ca-cert.pem`。daemon 退出时 sidecar 一并停止。代理对 TCP 建连和两端 TLS 握手设置 15 秒期限,对无读写活动的连接设置 120 秒空闲期限;超时、报错或任一端关闭时释放两端连接。持续有数据的 SSE 流不受总时长限制,连续静默超过 120 秒的流会断开。
|
|
41
41
|
|
|
42
42
|
`connect` 支持 `--workspace-root <path>` 指定 dispatch 仓库与任务工作区所在目录(默认 `~/autowonder_workspaces`)。该参数会转发给 daemon,等价于设置 `AUTOWONDER_WORKSPACE_ROOT` 环境变量,适合把工作区放在非默认路径的执行机上(如 Windows 的 `D:\autowonder`)。
|
|
43
43
|
|
|
@@ -158,7 +158,7 @@ mv "$queue/.assignment.tmp" "$queue/assignment.json"
|
|
|
158
158
|
也可以直接提交到本地 API:
|
|
159
159
|
|
|
160
160
|
```bash
|
|
161
|
-
npx -y autowonder@0.3.
|
|
161
|
+
npx -y autowonder@0.3.6 dispatch ./assignment.json
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
## 上传工单附件
|
|
@@ -166,7 +166,7 @@ npx -y autowonder@0.3.5 dispatch ./assignment.json
|
|
|
166
166
|
把本地需求/设计文档直接上传到工单,避免在会话里传递大文件:
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
|
-
npx -y autowonder@0.3.
|
|
169
|
+
npx -y autowonder@0.3.6 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
上传令牌通过 MCP 工具 `autowonder.workitem_cli_upload_token` 签发,经 `--token` 或 `AUTOWONDER_UPLOAD_TOKEN` 环境变量传入。`--file` 可重复指定多个文件;`--json` 输出机器可读结果。失败时按错误类型返回不同退出码(401→3、403→4、404→5、409→6、413→7、其他 4xx→8、网络/重定向→9)。
|
|
@@ -176,7 +176,7 @@ npx -y autowonder@0.3.5 workitem upload --server-url <autowonder-server-url> --w
|
|
|
176
176
|
把工单上已有的需求/设计文档下载到本地目录,再用本地文件工具阅读,避免把大文件或二进制附件搬进会话上下文:
|
|
177
177
|
|
|
178
178
|
```bash
|
|
179
|
-
npx -y autowonder@0.3.
|
|
179
|
+
npx -y autowonder@0.3.6 workitem download --server-url <autowonder-server-url> --workitem-id <id> --file <name-or-id> --output-dir <dir> --json
|
|
180
180
|
```
|
|
181
181
|
|
|
182
182
|
下载令牌通过 MCP 工具 `autowonder.workitem_cli_download_token` 签发(与上传令牌相互独立),经 `--token` 或 `AUTOWONDER_DOWNLOAD_TOKEN` 环境变量传入。`--file` 可重复指定文档名或 artifact id,省略则下载该工单的全部附件;`--output-dir` 默认为当前目录;`--json` 输出机器可读结果,含每个文件的 `id`、`name`、`path`、`size`。退出码约定与 `workitem upload` 相同(401→3、403→4、404→5、409→6、413→7、其他 4xx→8、网络/重定向→9)。
|
|
@@ -186,7 +186,7 @@ npx -y autowonder@0.3.5 workitem download --server-url <autowonder-server-url> -
|
|
|
186
186
|
把需求/设计文档上传到 7x24 定时任务,用法与工单附件一致(同一套上传令牌与预检规则):
|
|
187
187
|
|
|
188
188
|
```bash
|
|
189
|
-
npx -y autowonder@0.3.
|
|
189
|
+
npx -y autowonder@0.3.6 scheduled-task upload --server-url <autowonder-server-url> --scheduled-task-id <id> --file <filepath-1> --file <filepath-2> --json
|
|
190
190
|
```
|
|
191
191
|
|
|
192
192
|
上传令牌同样通过 `autowonder.workitem_cli_upload_token` 签发(令牌为用户级,对当前可修改的任意定时任务生效),经 `--token` 或 `AUTOWONDER_UPLOAD_TOKEN` 传入;退出码约定与 `workitem upload` 相同。
|
|
@@ -194,8 +194,8 @@ npx -y autowonder@0.3.5 scheduled-task upload --server-url <autowonder-server-ur
|
|
|
194
194
|
## 管理 daemon
|
|
195
195
|
|
|
196
196
|
```bash
|
|
197
|
-
npx -y autowonder@0.3.
|
|
198
|
-
npx -y autowonder@0.3.
|
|
197
|
+
npx -y autowonder@0.3.6 status
|
|
198
|
+
npx -y autowonder@0.3.6 stop
|
|
199
199
|
```
|
|
200
200
|
|
|
201
201
|
默认 API 是 `http://127.0.0.1:34989`,日志位于 `~/.autowonder/daemon.log`。npm 包不包含任何 agent、MCP 或服务端凭证。
|
|
@@ -203,7 +203,7 @@ npx -y autowonder@0.3.5 stop
|
|
|
203
203
|
## 调试
|
|
204
204
|
|
|
205
205
|
```bash
|
|
206
|
-
npx -y autowonder@0.3.
|
|
206
|
+
npx -y autowonder@0.3.6 connect ... --debug
|
|
207
207
|
```
|
|
208
208
|
|
|
209
209
|
`--debug` 会让 CLI 以 `AUTOWONDER_DEBUG=1` 启动 daemon,把完整的 daemon 与 agent 活动(runtime 事件流、provider 事件解码、重试退避与会话门等待、空闲时的阻塞点)同时输出到控制台和会话调试日志。调试日志不做脱敏,仅用于本机排障;不传 `--debug` 时该环境变量不会注入。
|
package/bin/token-proxy.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* 降级策略:
|
|
15
15
|
* - 所有解析逻辑被 try-catch 包裹,解析失败只丢 token 数据,不断流量
|
|
16
16
|
* - proxy 启动失败时,wrapper 不注入 HTTPS_PROXY,daemon 正常直连
|
|
17
|
-
* -
|
|
17
|
+
* - 运行中的代理连接失败会关闭隧道,由客户端处理重试;不承诺自动直连
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
'use strict';
|
|
@@ -316,8 +316,53 @@ function extractDispatchInfo(req) {
|
|
|
316
316
|
|
|
317
317
|
// ─── Forward Proxy (CONNECT method) ─────────────────────────────────────────
|
|
318
318
|
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
// Bound connection setup separately from idle time: a peer can trickle bytes
|
|
320
|
+
// during TLS negotiation, so a socket inactivity timeout alone is insufficient.
|
|
321
|
+
function guardConnection(sockets, { readyEvents, connectTimeoutMs = 15000,
|
|
322
|
+
idleTimeoutMs = 120000, onCleanup = () => {}, onError = () => {} }) {
|
|
323
|
+
let closed = false;
|
|
324
|
+
const pending = new Set(sockets);
|
|
325
|
+
const cleanup = () => {
|
|
326
|
+
if (closed) return;
|
|
327
|
+
closed = true;
|
|
328
|
+
clearTimeout(connectTimer);
|
|
329
|
+
for (const socket of sockets) {
|
|
330
|
+
socket.setTimeout(0);
|
|
331
|
+
socket.destroy();
|
|
332
|
+
}
|
|
333
|
+
onCleanup();
|
|
334
|
+
};
|
|
335
|
+
const connectTimer = setTimeout(() => {
|
|
336
|
+
onError(new Error('proxy connection setup timed out'));
|
|
337
|
+
cleanup();
|
|
338
|
+
}, connectTimeoutMs);
|
|
339
|
+
connectTimer.unref();
|
|
340
|
+
sockets.forEach((socket, i) => {
|
|
341
|
+
socket.setTimeout(idleTimeoutMs);
|
|
342
|
+
socket.on('timeout', () => {
|
|
343
|
+
onError(new Error('proxy connection idle timeout'));
|
|
344
|
+
cleanup();
|
|
345
|
+
});
|
|
346
|
+
socket.on('close', cleanup);
|
|
347
|
+
socket.on('error', error => { onError(error); cleanup(); });
|
|
348
|
+
// Flush queued bytes before closing the other half. The idle timer still
|
|
349
|
+
// bounds a peer that never completes shutdown.
|
|
350
|
+
socket.on('end', () => {
|
|
351
|
+
for (const peer of sockets) if (peer !== socket) peer.end();
|
|
352
|
+
});
|
|
353
|
+
const readyEvent = readyEvents[i];
|
|
354
|
+
if (!readyEvent) pending.delete(socket);
|
|
355
|
+
else socket.once(readyEvent, () => {
|
|
356
|
+
pending.delete(socket);
|
|
357
|
+
if (!pending.size) clearTimeout(connectTimer);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
if (!pending.size) clearTimeout(connectTimer);
|
|
361
|
+
return cleanup;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function createProxyServer({ ca = ensureCA(), hostCertificate = getHostCert,
|
|
365
|
+
connectTimeoutMs = 15000, idleTimeoutMs = 120000, sniffHosts = SNIFF_HOSTS } = {}) {
|
|
321
366
|
const dbg = DEBUG ? (...args) => console.log(...args) : () => {};
|
|
322
367
|
|
|
323
368
|
const server = http.createServer((req, res) => {
|
|
@@ -332,36 +377,26 @@ function startProxy(port) {
|
|
|
332
377
|
const ts = () => new Date().toISOString().slice(11, 23);
|
|
333
378
|
const [hostname, portStr] = req.url.split(':');
|
|
334
379
|
const remotePort = parseInt(portStr) || 443;
|
|
335
|
-
const shouldSniff =
|
|
380
|
+
const shouldSniff = sniffHosts.has(hostname);
|
|
336
381
|
const { dispatchId, stepId } = extractDispatchInfo(req);
|
|
337
382
|
|
|
338
383
|
dbg(`[${ts()}] CONNECT #${seq} ${hostname}:${remotePort} sniff=${shouldSniff} dispatch=${dispatchId || 'none'} step=${stepId || 'none'}`);
|
|
339
384
|
|
|
340
|
-
|
|
341
|
-
dbg(`[${ts()}] #${seq} → BLIND TUNNEL (not in SNIFF_HOSTS)`);
|
|
342
|
-
const upstream = net.connect(remotePort, hostname, () => {
|
|
343
|
-
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
344
|
-
upstream.write(head);
|
|
345
|
-
upstream.pipe(clientSocket);
|
|
346
|
-
clientSocket.pipe(upstream);
|
|
347
|
-
});
|
|
348
|
-
upstream.on('error', (e) => { dbg(`[${ts()}] #${seq} blind upstream err: ${e.message}`); try { clientSocket.end(); } catch (_) {} });
|
|
349
|
-
clientSocket.on('error', (e) => { dbg(`[${ts()}] #${seq} blind client err: ${e.message}`); try { upstream.end(); } catch (_) {} });
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// MITM for Qoder API hosts: decrypt, sniff usage, re-encrypt
|
|
354
|
-
const hostCert = getHostCert(hostname, ca);
|
|
385
|
+
const hostCert = shouldSniff ? hostCertificate(hostname, ca) : null;
|
|
355
386
|
if (!hostCert) {
|
|
356
|
-
dbg(`[${ts()}] #${seq} → BLIND TUNNEL
|
|
357
|
-
const upstream = net.connect(remotePort, hostname
|
|
387
|
+
dbg(`[${ts()}] #${seq} → BLIND TUNNEL`);
|
|
388
|
+
const upstream = net.connect(remotePort, hostname);
|
|
389
|
+
guardConnection([clientSocket, upstream], {
|
|
390
|
+
readyEvents: [null, 'connect'], connectTimeoutMs, idleTimeoutMs,
|
|
391
|
+
onError: e => dbg(`[${ts()}] #${seq} tunnel error: ${e.message}`),
|
|
392
|
+
});
|
|
393
|
+
upstream.once('connect', () => {
|
|
394
|
+
if (clientSocket.destroyed) return;
|
|
358
395
|
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
359
|
-
upstream.write(head);
|
|
396
|
+
if (head.length) upstream.write(head);
|
|
360
397
|
upstream.pipe(clientSocket);
|
|
361
398
|
clientSocket.pipe(upstream);
|
|
362
399
|
});
|
|
363
|
-
upstream.on('error', (e) => { dbg(`[${ts()}] #${seq} cert-fail upstream err: ${e.message}`); try { clientSocket.end(); } catch (_) {} });
|
|
364
|
-
clientSocket.on('error', (e) => { dbg(`[${ts()}] #${seq} cert-fail client err: ${e.message}`); try { upstream.end(); } catch (_) {} });
|
|
365
400
|
return;
|
|
366
401
|
}
|
|
367
402
|
|
|
@@ -370,6 +405,8 @@ function startProxy(port) {
|
|
|
370
405
|
// Accept TLS from client (presenting our per-host cert signed by our CA)
|
|
371
406
|
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
372
407
|
|
|
408
|
+
// CONNECT head contains encrypted TLS bytes, not plaintext HTTP.
|
|
409
|
+
if (head.length) clientSocket.unshift(head);
|
|
373
410
|
const tlsClient = new tls.TLSSocket(clientSocket, {
|
|
374
411
|
isServer: true,
|
|
375
412
|
key: hostCert.key,
|
|
@@ -385,7 +422,6 @@ function startProxy(port) {
|
|
|
385
422
|
let tlsUpstreamReady = false;
|
|
386
423
|
tlsUpstream.on('secureConnect', () => { tlsUpstreamReady = true; dbg(`[${ts()}] #${seq} TLS-upstream handshake OK → ${hostname}`); });
|
|
387
424
|
|
|
388
|
-
let responseBuffer = Buffer.alloc(0);
|
|
389
425
|
let usageCount = 0;
|
|
390
426
|
let totalBytes = 0;
|
|
391
427
|
let rawFd = null;
|
|
@@ -429,7 +465,6 @@ function startProxy(port) {
|
|
|
429
465
|
|
|
430
466
|
tlsUpstream.on('data', (chunk) => {
|
|
431
467
|
totalBytes += chunk.length;
|
|
432
|
-
try { tlsClient.write(chunk); } catch (_) {}
|
|
433
468
|
if (rawFd !== null) { try { fs.writeSync(rawFd, chunk); } catch (_) {} }
|
|
434
469
|
try {
|
|
435
470
|
const text = lineRemainder + chunk.toString('utf8');
|
|
@@ -444,9 +479,9 @@ function startProxy(port) {
|
|
|
444
479
|
} catch (_) {}
|
|
445
480
|
});
|
|
446
481
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
482
|
+
// pipe() handles backpressure; accounting only observes the stream.
|
|
483
|
+
tlsUpstream.pipe(tlsClient);
|
|
484
|
+
tlsClient.pipe(tlsUpstream);
|
|
450
485
|
|
|
451
486
|
const cleanup = () => {
|
|
452
487
|
// Final extraction from any remaining data (lineRemainder may hold last line without trailing \n)
|
|
@@ -456,18 +491,24 @@ function startProxy(port) {
|
|
|
456
491
|
lineRemainder = '';
|
|
457
492
|
dbg(`[${ts()}] #${seq} CLOSED ${hostname} bytes=${totalBytes} usages=${usageCount} tlsClient=${tlsClientReady} tlsUpstream=${tlsUpstreamReady}`);
|
|
458
493
|
if (rawFd !== null) { try { fs.closeSync(rawFd); } catch (_) {} rawFd = null; }
|
|
459
|
-
try { tlsClient.end(); } catch (_) {}
|
|
460
|
-
try { tlsUpstream.end(); } catch (_) {}
|
|
461
494
|
};
|
|
462
495
|
|
|
463
|
-
tlsClient
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
496
|
+
const closeConnection = guardConnection([tlsClient, tlsUpstream], {
|
|
497
|
+
readyEvents: ['secure', 'secureConnect'], connectTimeoutMs, idleTimeoutMs,
|
|
498
|
+
onCleanup: cleanup,
|
|
499
|
+
onError: e => dbg(`[${ts()}] #${seq} TLS error: ${e.message}`),
|
|
500
|
+
});
|
|
501
|
+
// Also fence an aborted raw CONNECT socket during TLS setup.
|
|
502
|
+
clientSocket.on('close', closeConnection);
|
|
503
|
+
clientSocket.on('error', closeConnection);
|
|
467
504
|
|
|
468
|
-
if (head.length > 0) tlsClient.emit('data', head);
|
|
469
505
|
});
|
|
470
506
|
|
|
507
|
+
return server;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function startProxy(port) {
|
|
511
|
+
const server = createProxyServer();
|
|
471
512
|
server.on('error', (err) => {
|
|
472
513
|
console.error(`[token-proxy] server error: ${err.message}`);
|
|
473
514
|
process.exit(1);
|
|
@@ -554,29 +595,30 @@ function parseDuration(s) {
|
|
|
554
595
|
const args = process.argv.slice(2);
|
|
555
596
|
const command = args[0];
|
|
556
597
|
|
|
557
|
-
if (
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
598
|
+
if (require.main === module) {
|
|
599
|
+
if (command === 'start') {
|
|
600
|
+
const portIdx = args.indexOf('--port');
|
|
601
|
+
const port = portIdx >= 0 ? parseInt(args[portIdx + 1]) : DEFAULT_PORT;
|
|
602
|
+
startProxy(port);
|
|
603
|
+
} else if (command === 'query') {
|
|
604
|
+
const jsonFlag = args.includes('--json');
|
|
605
|
+
const sinceIdx = args.indexOf('--since');
|
|
606
|
+
const since = sinceIdx >= 0 ? args[sinceIdx + 1] : '24h';
|
|
607
|
+
queryUsage({ json: jsonFlag, since });
|
|
608
|
+
} else {
|
|
609
|
+
console.log(`Usage:
|
|
610
|
+
node token-proxy.js start [--port ${DEFAULT_PORT}] Start the proxy
|
|
611
|
+
node token-proxy.js query [--since 1h] [--json] Query token usage
|
|
612
|
+
|
|
613
|
+
Environment (set before starting daemon):
|
|
614
|
+
HTTPS_PROXY=http://127.0.0.1:${DEFAULT_PORT}
|
|
615
|
+
NODE_EXTRA_CA_CERTS=${CA_CERT_PATH}
|
|
616
|
+
|
|
617
|
+
Files:
|
|
618
|
+
CA cert: ${CA_CERT_PATH}
|
|
619
|
+
Usage log: ${USAGE_LOG_PATH}`);
|
|
620
|
+
}
|
|
578
621
|
}
|
|
579
622
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
623
|
+
module.exports = { extractUsageFromResponse, extractDispatchInfo, buildUsageRecord,
|
|
624
|
+
guardConnection, createProxyServer };
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|