@acosmi/sdk-ts 1.0.1 → 1.0.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/CHANGELOG.md +58 -0
- package/README.md +3 -2
- package/dist/browser/index.mjs +190 -57
- package/dist/browser/index.mjs.map +1 -1
- package/dist/index.mjs +190 -57
- package/dist/index.mjs.map +1 -1
- package/dist/node/index.cjs +190 -56
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +48 -3
- package/dist/node/index.d.ts +48 -3
- package/dist/node/index.mjs +190 -57
- package/dist/node/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/node/index.cjs
CHANGED
|
@@ -1405,9 +1405,18 @@ function skillScopes() {
|
|
|
1405
1405
|
}
|
|
1406
1406
|
|
|
1407
1407
|
// src/store.ts
|
|
1408
|
+
var fileLockDefaults = {
|
|
1409
|
+
/** 获取锁的超时上限 (毫秒). refresh 流程含网络 < 30s, 30s 已远超正常完成时间. */
|
|
1410
|
+
acquireTimeoutMs: 3e4,
|
|
1411
|
+
/** 旧锁判定阈值 (毫秒). 锁文件 mtime 早于此值视为 stale 进程崩溃残留, 自动 break. */
|
|
1412
|
+
staleMs: 6e4,
|
|
1413
|
+
/** 重试间隔基数 (毫秒). 真实间隔 = base + random(0, jitter). */
|
|
1414
|
+
retryBaseMs: 30,
|
|
1415
|
+
retryJitterMs: 70
|
|
1416
|
+
};
|
|
1408
1417
|
var FileTokenStore = class {
|
|
1409
1418
|
path;
|
|
1410
|
-
/**
|
|
1419
|
+
/** 进程内串行化 (Promise chain) — 与跨进程 flock 配合, 避免单进程内并发持锁产生死锁式互等. */
|
|
1411
1420
|
chain = Promise.resolve();
|
|
1412
1421
|
constructor(path) {
|
|
1413
1422
|
if (typeof process === "undefined" || !process.versions || !process.versions.node) {
|
|
@@ -1426,7 +1435,9 @@ var FileTokenStore = class {
|
|
|
1426
1435
|
this.path = path.join(os.homedir(), ".acosmi", "tokens.json");
|
|
1427
1436
|
return this.path;
|
|
1428
1437
|
}
|
|
1429
|
-
|
|
1438
|
+
/** 进程内串行 (维持 v1.0.1 单进程语义). flock 之外另一层防御: 如果用户自己就是单进程
|
|
1439
|
+
* 并发场景, 不必每次都进 flock 旁路文件 IO. */
|
|
1440
|
+
withChain(fn) {
|
|
1430
1441
|
const next = this.chain.then(fn, fn);
|
|
1431
1442
|
this.chain = next.then(
|
|
1432
1443
|
() => void 0,
|
|
@@ -1434,19 +1445,106 @@ var FileTokenStore = class {
|
|
|
1434
1445
|
);
|
|
1435
1446
|
return next;
|
|
1436
1447
|
}
|
|
1448
|
+
/**
|
|
1449
|
+
* 跨进程临界区. 用 sidecar `<path>.lock` 文件 + O_EXCL 创建语义实现互斥:
|
|
1450
|
+
* - 创建成功 = 持有锁; 写入 pid+timestamp 便于诊断
|
|
1451
|
+
* - 创建失败 (EEXIST) = 别的进程持锁, backoff 重试
|
|
1452
|
+
* - 锁文件 mtime > staleMs = 进程崩溃残留, unlink 后重试
|
|
1453
|
+
* - acquireTimeoutMs 超时 = 抛错 (caller 应作为 transient error 处理, 上层 retry)
|
|
1454
|
+
*
|
|
1455
|
+
* 注意: O_EXCL 在 NFS 上不保证原子; FileTokenStore 适用于本地文件系统 (典型用户家目录).
|
|
1456
|
+
* 真要跨机共享 token, 应实现自定义 Keychain / 数据库 store, 不要用 FileTokenStore.
|
|
1457
|
+
*/
|
|
1458
|
+
async withLock(fn) {
|
|
1459
|
+
const fs = await import('fs/promises');
|
|
1460
|
+
const pathMod = await import('path');
|
|
1461
|
+
const p = await this.resolvePath();
|
|
1462
|
+
const lockPath = `${p}.lock`;
|
|
1463
|
+
const dir = pathMod.dirname(p);
|
|
1464
|
+
await fs.mkdir(dir, { recursive: true, mode: 448 });
|
|
1465
|
+
const startMs = Date.now();
|
|
1466
|
+
let release = null;
|
|
1467
|
+
while (true) {
|
|
1468
|
+
try {
|
|
1469
|
+
const fh = await fs.open(lockPath, "wx", 384);
|
|
1470
|
+
try {
|
|
1471
|
+
await fh.writeFile(`${process.pid}
|
|
1472
|
+
${Date.now()}
|
|
1473
|
+
`, "utf8");
|
|
1474
|
+
} finally {
|
|
1475
|
+
await fh.close();
|
|
1476
|
+
}
|
|
1477
|
+
release = async () => {
|
|
1478
|
+
try {
|
|
1479
|
+
await fs.unlink(lockPath);
|
|
1480
|
+
} catch {
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
break;
|
|
1484
|
+
} catch (e) {
|
|
1485
|
+
if (!isAlreadyExistsError(e)) throw e;
|
|
1486
|
+
let stale = false;
|
|
1487
|
+
try {
|
|
1488
|
+
const st = await fs.stat(lockPath);
|
|
1489
|
+
if (Date.now() - st.mtimeMs > fileLockDefaults.staleMs) stale = true;
|
|
1490
|
+
} catch {
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
if (stale) {
|
|
1494
|
+
try {
|
|
1495
|
+
await fs.unlink(lockPath);
|
|
1496
|
+
} catch {
|
|
1497
|
+
}
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
if (Date.now() - startMs > fileLockDefaults.acquireTimeoutMs) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
`acquire token file lock timeout (${fileLockDefaults.acquireTimeoutMs}ms): ${lockPath}`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
const waitMs = fileLockDefaults.retryBaseMs + Math.random() * fileLockDefaults.retryJitterMs;
|
|
1506
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
try {
|
|
1510
|
+
return await fn();
|
|
1511
|
+
} finally {
|
|
1512
|
+
if (release) await release();
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* 写入 token. 流程:
|
|
1517
|
+
* 1. mkdir -p (默认路径目录)
|
|
1518
|
+
* 2. 写入 `<path>.tmp.<pid>`
|
|
1519
|
+
* 3. fsync 后 rename 到正式路径 (POSIX 上 rename(2) 同分区原子, Windows 上 ReplaceFile)
|
|
1520
|
+
*
|
|
1521
|
+
* 选择 atomic rename 而非直接 writeFile: 多进程并发或本进程崩溃时, 读端永远看到的是
|
|
1522
|
+
* 完整的旧/新 JSON, 不会读到截断半文件 (Client.create.store.load 可能在另一进程
|
|
1523
|
+
* 写入中间触发, atomic rename 避免它解析失败).
|
|
1524
|
+
*/
|
|
1437
1525
|
save(tokens) {
|
|
1438
|
-
return this.
|
|
1526
|
+
return this.withChain(async () => {
|
|
1439
1527
|
const fs = await import('fs/promises');
|
|
1440
|
-
const
|
|
1528
|
+
const pathMod = await import('path');
|
|
1441
1529
|
const p = await this.resolvePath();
|
|
1442
|
-
const dir =
|
|
1530
|
+
const dir = pathMod.dirname(p);
|
|
1443
1531
|
await fs.mkdir(dir, { recursive: true, mode: 448 });
|
|
1532
|
+
const tmp = `${p}.tmp.${process.pid}.${Date.now()}.${Math.floor(Math.random() * 1e6)}`;
|
|
1444
1533
|
const data = JSON.stringify(tokens, null, 2);
|
|
1445
|
-
await fs.writeFile(
|
|
1534
|
+
await fs.writeFile(tmp, data, { encoding: "utf8", mode: 384 });
|
|
1535
|
+
try {
|
|
1536
|
+
await fs.rename(tmp, p);
|
|
1537
|
+
} catch (e) {
|
|
1538
|
+
try {
|
|
1539
|
+
await fs.unlink(tmp);
|
|
1540
|
+
} catch {
|
|
1541
|
+
}
|
|
1542
|
+
throw e;
|
|
1543
|
+
}
|
|
1446
1544
|
});
|
|
1447
1545
|
}
|
|
1448
1546
|
load() {
|
|
1449
|
-
return this.
|
|
1547
|
+
return this.withChain(async () => {
|
|
1450
1548
|
const fs = await import('fs/promises');
|
|
1451
1549
|
const p = await this.resolvePath();
|
|
1452
1550
|
try {
|
|
@@ -1461,7 +1559,7 @@ var FileTokenStore = class {
|
|
|
1461
1559
|
});
|
|
1462
1560
|
}
|
|
1463
1561
|
clear() {
|
|
1464
|
-
return this.
|
|
1562
|
+
return this.withChain(async () => {
|
|
1465
1563
|
const fs = await import('fs/promises');
|
|
1466
1564
|
const p = await this.resolvePath();
|
|
1467
1565
|
try {
|
|
@@ -1479,6 +1577,12 @@ function isNotExistError(e) {
|
|
|
1479
1577
|
}
|
|
1480
1578
|
return false;
|
|
1481
1579
|
}
|
|
1580
|
+
function isAlreadyExistsError(e) {
|
|
1581
|
+
if (typeof e === "object" && e !== null && "code" in e) {
|
|
1582
|
+
return e.code === "EEXIST";
|
|
1583
|
+
}
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1482
1586
|
function newFileTokenStore(path) {
|
|
1483
1587
|
return new FileTokenStore(path);
|
|
1484
1588
|
}
|
|
@@ -2345,61 +2449,67 @@ var Client = class _Client {
|
|
|
2345
2449
|
if (!tokenSetIsExpired(tokens)) {
|
|
2346
2450
|
return tokens.access_token;
|
|
2347
2451
|
}
|
|
2348
|
-
return this.withMu(
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2452
|
+
return this.withMu(
|
|
2453
|
+
() => this.storeWithLock(async () => {
|
|
2454
|
+
await this.syncFromDisk();
|
|
2455
|
+
if (this.tokens == null) {
|
|
2456
|
+
throw new Error("not authorized, call login() first");
|
|
2457
|
+
}
|
|
2458
|
+
if (!tokenSetIsExpired(this.tokens)) {
|
|
2459
|
+
return this.tokens.access_token;
|
|
2460
|
+
}
|
|
2461
|
+
if (this.meta == null) {
|
|
2462
|
+
try {
|
|
2463
|
+
this.meta = await discover(this.serverURL, signal);
|
|
2464
|
+
} catch (e) {
|
|
2465
|
+
throw new Error(
|
|
2466
|
+
`discover for refresh: ${e instanceof Error ? e.message : String(e)}`
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
let tokenResp;
|
|
2356
2471
|
try {
|
|
2357
|
-
|
|
2472
|
+
tokenResp = await refreshToken(this.meta, this.tokens.client_id, this.tokens.refresh_token, signal);
|
|
2358
2473
|
} catch (e) {
|
|
2359
|
-
throw new Error(
|
|
2360
|
-
`discover for refresh: ${e instanceof Error ? e.message : String(e)}`
|
|
2361
|
-
);
|
|
2474
|
+
throw new Error(`refresh token: ${e instanceof Error ? e.message : String(e)}`);
|
|
2362
2475
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
await this.store.save(this.tokens);
|
|
2373
|
-
} catch (e) {
|
|
2374
|
-
console.warn(`[acosmi-sdk] warning: save refreshed token failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2375
|
-
}
|
|
2376
|
-
return this.tokens.access_token;
|
|
2377
|
-
});
|
|
2476
|
+
this.tokens = newTokenSet(tokenResp, this.tokens.client_id, this.serverURL);
|
|
2477
|
+
try {
|
|
2478
|
+
await this.store.save(this.tokens);
|
|
2479
|
+
} catch (e) {
|
|
2480
|
+
console.warn(`[acosmi-sdk] warning: save refreshed token failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2481
|
+
}
|
|
2482
|
+
return this.tokens.access_token;
|
|
2483
|
+
})
|
|
2484
|
+
);
|
|
2378
2485
|
}
|
|
2379
2486
|
/** 强制刷新 token (用于 401 重试) */
|
|
2380
2487
|
async forceRefresh(signal) {
|
|
2381
|
-
return this.withMu(
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
try {
|
|
2396
|
-
await this.store.save(this.tokens);
|
|
2397
|
-
} catch (e) {
|
|
2398
|
-
console.warn(
|
|
2399
|
-
`[acosmi-sdk] warning: save refreshed token failed: ${e instanceof Error ? e.message : String(e)}`
|
|
2488
|
+
return this.withMu(
|
|
2489
|
+
() => this.storeWithLock(async () => {
|
|
2490
|
+
await this.syncFromDisk();
|
|
2491
|
+
if (this.tokens == null) {
|
|
2492
|
+
throw new Error("no tokens to refresh");
|
|
2493
|
+
}
|
|
2494
|
+
if (this.meta == null) {
|
|
2495
|
+
this.meta = await discover(this.serverURL, signal);
|
|
2496
|
+
}
|
|
2497
|
+
const tokenResp = await refreshToken(
|
|
2498
|
+
this.meta,
|
|
2499
|
+
this.tokens.client_id,
|
|
2500
|
+
this.tokens.refresh_token,
|
|
2501
|
+
signal
|
|
2400
2502
|
);
|
|
2401
|
-
|
|
2402
|
-
|
|
2503
|
+
this.tokens = newTokenSet(tokenResp, this.tokens.client_id, this.serverURL);
|
|
2504
|
+
try {
|
|
2505
|
+
await this.store.save(this.tokens);
|
|
2506
|
+
} catch (e) {
|
|
2507
|
+
console.warn(
|
|
2508
|
+
`[acosmi-sdk] warning: save refreshed token failed: ${e instanceof Error ? e.message : String(e)}`
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
})
|
|
2512
|
+
);
|
|
2403
2513
|
}
|
|
2404
2514
|
/** 互斥锁 helper (替代 Go sync.Mutex) */
|
|
2405
2515
|
withMu(fn) {
|
|
@@ -2410,6 +2520,29 @@ var Client = class _Client {
|
|
|
2410
2520
|
);
|
|
2411
2521
|
return next;
|
|
2412
2522
|
}
|
|
2523
|
+
/** v1.0.2: 跨进程临界区 helper. store.withLock 可选, 缺省则直接调用 fn (LocalStorage /
|
|
2524
|
+
* InMemory 单进程无需). */
|
|
2525
|
+
async storeWithLock(fn) {
|
|
2526
|
+
const lock = this.store.withLock;
|
|
2527
|
+
if (typeof lock === "function") {
|
|
2528
|
+
return lock.call(this.store, fn);
|
|
2529
|
+
}
|
|
2530
|
+
return fn();
|
|
2531
|
+
}
|
|
2532
|
+
/** v1.0.2: 从磁盘同步 token (refresh 前). 别的进程 rotation 后磁盘 refresh_token 已变,
|
|
2533
|
+
* 本进程内存仍是旧 R0; 不同步直接 refresh 必撞网关 400. load 失败保留内存继续 (容错). */
|
|
2534
|
+
async syncFromDisk() {
|
|
2535
|
+
let onDisk = null;
|
|
2536
|
+
try {
|
|
2537
|
+
onDisk = await this.store.load();
|
|
2538
|
+
} catch {
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
if (!onDisk) return;
|
|
2542
|
+
if (this.tokens == null || onDisk.refresh_token !== this.tokens.refresh_token) {
|
|
2543
|
+
this.tokens = onDisk;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2413
2546
|
// ===========================================================================
|
|
2414
2547
|
// Managed Models
|
|
2415
2548
|
// ===========================================================================
|
|
@@ -4002,6 +4135,7 @@ exports.discover = discover;
|
|
|
4002
4135
|
exports.effectivePolicy = effectivePolicy;
|
|
4003
4136
|
exports.exchangeCode = exchangeCode;
|
|
4004
4137
|
exports.extractAnthropicBlockMeta = extractAnthropicBlockMeta;
|
|
4138
|
+
exports.fileLockDefaults = fileLockDefaults;
|
|
4005
4139
|
exports.getAdapter = getAdapter;
|
|
4006
4140
|
exports.getAdapterForModel = getAdapterForModel;
|
|
4007
4141
|
exports.isSSLError = isSSLError;
|