@vibedev-si/dsh-vibedev 0.2.6 → 0.3.0
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 +16 -2
- package/THIRD-PARTY-NOTICES.txt +1 -1
- package/client/client.js +127 -22
- package/lib/account/index.d.ts +3 -1
- package/lib/auth/credentials.d.ts +5 -3
- package/lib/auth/login.d.ts +12 -2
- package/lib/auth/sign-in-state.d.ts +21 -0
- package/lib/gateway/catalog.d.ts +7 -0
- package/lib/gateway/errors.d.ts +15 -0
- package/lib/gateway/http.d.ts +1 -1
- package/lib/gateway/origin.d.ts +10 -0
- package/lib/index.js +569 -198
- package/lib/tasks/tracker.d.ts +46 -18
- package/lib/tools/account.d.ts +0 -2
- package/lib/tools/common.d.ts +1 -1
- package/lib/video/plan.d.ts +5 -2
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -53,12 +53,14 @@ var AccountService = class {
|
|
|
53
53
|
const credential = await this.options.chain.resolve().catch(() => void 0);
|
|
54
54
|
const source = credential === void 0 ? "none" : credential.kind === "account" ? "host" : credential.kind;
|
|
55
55
|
const pending = this.options.login.pendingSignIn();
|
|
56
|
+
const attempt = this.options.login.signInAttempt();
|
|
56
57
|
const base = {
|
|
57
58
|
source,
|
|
58
59
|
models: this.options.models(),
|
|
59
60
|
links: this.links(),
|
|
60
61
|
primary: this.options.primary === true,
|
|
61
|
-
...pending === void 0 ? {} : { pending }
|
|
62
|
+
...pending === void 0 ? {} : { pending },
|
|
63
|
+
...attempt === void 0 ? {} : { attempt }
|
|
62
64
|
};
|
|
63
65
|
if (credential === void 0) return base;
|
|
64
66
|
const grantUser = credential.kind === "plugin" ? await this.options.login.user().catch(() => void 0) : void 0;
|
|
@@ -82,7 +84,8 @@ var AccountService = class {
|
|
|
82
84
|
authorization: `Bearer ${token}`,
|
|
83
85
|
"user-agent": this.options.userAgent
|
|
84
86
|
},
|
|
85
|
-
signal: AbortSignal.timeout(15e3)
|
|
87
|
+
signal: AbortSignal.timeout(15e3),
|
|
88
|
+
redirect: "error"
|
|
86
89
|
});
|
|
87
90
|
if (!response.ok) return {};
|
|
88
91
|
const body = record$8(await response.json().catch(() => void 0));
|
|
@@ -166,9 +169,11 @@ function json(status, body) {
|
|
|
166
169
|
});
|
|
167
170
|
}
|
|
168
171
|
function failure(error) {
|
|
172
|
+
const candidate = record$8(error)?.code;
|
|
173
|
+
const code = candidate === "SIGN_IN_REFUSED" || candidate === "SIGN_IN_TIMEOUT" || candidate === "SIGN_IN_CANCELLED" ? candidate : "INTERNAL";
|
|
169
174
|
return json(500, { error: {
|
|
170
|
-
code
|
|
171
|
-
message:
|
|
175
|
+
code,
|
|
176
|
+
message: code === "SIGN_IN_CANCELLED" ? "Sign-in was cancelled. 登录已取消。" : code === "SIGN_IN_TIMEOUT" ? "Sign-in expired. 登录已超时,请重新登录。" : "The account request could not be completed. 账号请求未完成,请查看登录诊断后重试。"
|
|
172
177
|
} });
|
|
173
178
|
}
|
|
174
179
|
/**
|
|
@@ -297,11 +302,15 @@ function parseGatewayFailure(body) {
|
|
|
297
302
|
availableCny: amount$1(source.available_cny),
|
|
298
303
|
pendingCny: amount$1(source.pending_cny)
|
|
299
304
|
};
|
|
305
|
+
const submissionState = text$7(source.submission_state) ?? text$7(root.submission_state);
|
|
306
|
+
const retryAction = text$7(source.retry_action) ?? text$7(root.retry_action);
|
|
300
307
|
return {
|
|
301
308
|
...code === void 0 ? {} : { code },
|
|
302
309
|
...message === void 0 ? {} : { message },
|
|
303
310
|
...field === void 0 ? {} : { field },
|
|
304
311
|
...typeof source.retryable === "boolean" ? { retryable: source.retryable } : {},
|
|
312
|
+
...submissionState === void 0 ? {} : { submissionState: submissionState.toLowerCase() },
|
|
313
|
+
...retryAction === void 0 ? {} : { retryAction: retryAction.toLowerCase() },
|
|
305
314
|
...rechargeUrl === void 0 ? {} : { rechargeUrl },
|
|
306
315
|
...Object.fromEntries(Object.entries({
|
|
307
316
|
...numbers,
|
|
@@ -507,6 +516,32 @@ async function downloadTo(response, temporary, maxBytes, signal) {
|
|
|
507
516
|
}
|
|
508
517
|
}
|
|
509
518
|
//#endregion
|
|
519
|
+
//#region src/gateway/origin.ts
|
|
520
|
+
/** Origin boundaries for accounts and durable media state. No endpoint fallback is defined here. */
|
|
521
|
+
/** Production API origin. Storage URLs are returned by the server, not derived from this address. */
|
|
522
|
+
const DEFAULT_GATEWAY_ORIGIN = "https://api.vibedev.studio";
|
|
523
|
+
/** Validate a bare HTTP(S) origin and canonicalize default ports, casing and trailing slash. */
|
|
524
|
+
function normalizeGatewayOrigin(value) {
|
|
525
|
+
const url = new URL(value.trim());
|
|
526
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") throw new Error("gatewayOrigin must be a bare HTTP(S) origin, without credentials, path, query or fragment.");
|
|
527
|
+
return url.origin;
|
|
528
|
+
}
|
|
529
|
+
/** Stable, filesystem-safe identity; ports and schemes remain separate boundaries. */
|
|
530
|
+
function gatewayId(origin) {
|
|
531
|
+
return createHash("sha256").update(normalizeGatewayOrigin(origin)).digest("hex");
|
|
532
|
+
}
|
|
533
|
+
/** Every gateway uses its own namespace; never read the unmarked historical root. */
|
|
534
|
+
function gatewayStateDirectory(root, origin) {
|
|
535
|
+
return join(root, "gateways", gatewayId(origin));
|
|
536
|
+
}
|
|
537
|
+
/** Ignore the old shared key variable; development requires an explicitly named, gateway-specific variable. */
|
|
538
|
+
function developmentKey(origin, name, env = process.env) {
|
|
539
|
+
normalizeGatewayOrigin(origin);
|
|
540
|
+
const key = name.trim();
|
|
541
|
+
if (key === "" || key === "VIBEDEV_GATEWAY_API_KEY") return void 0;
|
|
542
|
+
return env[key]?.trim() || void 0;
|
|
543
|
+
}
|
|
544
|
+
//#endregion
|
|
510
545
|
//#region src/auth/credentials.ts
|
|
511
546
|
/**
|
|
512
547
|
* Which VibeDev credential a gateway request carries, in order: the host's
|
|
@@ -551,7 +586,10 @@ var CredentialChain = class {
|
|
|
551
586
|
else if (credential.kind === "plugin") await this.options.plugin?.reject(credential.token);
|
|
552
587
|
}
|
|
553
588
|
};
|
|
554
|
-
|
|
589
|
+
/** Credential identity for one gateway; never consult another gateway's record. */
|
|
590
|
+
function gatewayGrantKey(origin) {
|
|
591
|
+
return credentialKey("dsh-vibedev", `gateway-${gatewayId(origin)}`);
|
|
592
|
+
}
|
|
555
593
|
function isGrant(value) {
|
|
556
594
|
const grant = value;
|
|
557
595
|
return typeof grant === "object" && grant !== null && typeof grant.accessToken === "string" && typeof grant.refreshToken === "string" && typeof grant.expiresAt === "number" && typeof grant.refreshExpiresAt === "number";
|
|
@@ -560,11 +598,23 @@ function isGrant(value) {
|
|
|
560
598
|
* Grant storage: the host's credential store when one is loaded (a grant
|
|
561
599
|
* record the store keeps verbatim), else a private file in the state
|
|
562
600
|
* directory. The device id is not a secret and lives in the state directory.
|
|
563
|
-
* @param
|
|
601
|
+
* @param root - the plugin state root; the storage selects an origin-specific directory itself.
|
|
564
602
|
* @param credentials - the host's credential store, when loaded.
|
|
603
|
+
* @param selectedOrigin - the issuing gateway; unmarked historical state is never adopted.
|
|
565
604
|
* @returns the storage.
|
|
566
605
|
*/
|
|
567
|
-
function grantStorage(
|
|
606
|
+
function grantStorage(root, credentials, selectedOrigin = DEFAULT_GATEWAY_ORIGIN) {
|
|
607
|
+
const origin = normalizeGatewayOrigin(selectedOrigin);
|
|
608
|
+
const key = gatewayGrantKey(origin);
|
|
609
|
+
const stateDir = gatewayStateDirectory(root, origin);
|
|
610
|
+
const readGrant = (value) => isGrant(value) && value.gatewayOrigin === origin ? value : void 0;
|
|
611
|
+
const bind = (grant) => {
|
|
612
|
+
if (grant.gatewayOrigin !== void 0 && grant.gatewayOrigin !== origin) throw new Error("Cannot store a grant issued by another gateway.");
|
|
613
|
+
return {
|
|
614
|
+
...grant,
|
|
615
|
+
gatewayOrigin: origin
|
|
616
|
+
};
|
|
617
|
+
};
|
|
568
618
|
const sessionFile = join(stateDir, "session.json");
|
|
569
619
|
const deviceFile = join(stateDir, "device.json");
|
|
570
620
|
let device;
|
|
@@ -577,9 +627,9 @@ function grantStorage(stateDir, credentials) {
|
|
|
577
627
|
throw error;
|
|
578
628
|
}
|
|
579
629
|
const parsed = JSON.parse(text);
|
|
580
|
-
return
|
|
630
|
+
return readGrant(parsed);
|
|
581
631
|
};
|
|
582
|
-
const writeFileGrant = (grant) => writeFileAtomic(sessionFile, new TextEncoder().encode(grant === void 0 ? "null" : JSON.stringify(grant)), { mode: 384 });
|
|
632
|
+
const writeFileGrant = (grant) => writeFileAtomic(sessionFile, new TextEncoder().encode(grant === void 0 ? "null" : JSON.stringify(bind(grant))), { mode: 384 });
|
|
583
633
|
const clearFileGrant = async () => {
|
|
584
634
|
try {
|
|
585
635
|
await stat(sessionFile);
|
|
@@ -595,9 +645,9 @@ function grantStorage(stateDir, credentials) {
|
|
|
595
645
|
} catch {}
|
|
596
646
|
};
|
|
597
647
|
const readHostGrant = async (store) => {
|
|
598
|
-
const stored = await store.readRecord(
|
|
648
|
+
const stored = await store.readRecord(key);
|
|
599
649
|
if (stored !== void 0) {
|
|
600
|
-
const grant = stored.kind === "grant"
|
|
650
|
+
const grant = stored.kind === "grant" ? readGrant(stored.payload) : void 0;
|
|
601
651
|
if (grant !== void 0) await clearFileGrantQuietly();
|
|
602
652
|
return grant;
|
|
603
653
|
}
|
|
@@ -608,15 +658,15 @@ function grantStorage(stateDir, credentials) {
|
|
|
608
658
|
throw error;
|
|
609
659
|
}
|
|
610
660
|
let adopted;
|
|
611
|
-
await store.modifyRecord(
|
|
661
|
+
await store.modifyRecord(key, async (current) => {
|
|
612
662
|
if (current !== void 0) {
|
|
613
|
-
adopted = current.kind === "grant"
|
|
663
|
+
adopted = current.kind === "grant" ? readGrant(current.payload) : void 0;
|
|
614
664
|
return;
|
|
615
665
|
}
|
|
616
666
|
adopted = await readFileGrant();
|
|
617
667
|
return adopted === void 0 ? void 0 : {
|
|
618
668
|
kind: "grant",
|
|
619
|
-
payload: adopted
|
|
669
|
+
payload: bind(adopted)
|
|
620
670
|
};
|
|
621
671
|
});
|
|
622
672
|
if (adopted !== void 0) await clearFileGrantQuietly();
|
|
@@ -632,11 +682,11 @@ function grantStorage(stateDir, credentials) {
|
|
|
632
682
|
if (store !== void 0) {
|
|
633
683
|
if (grant === void 0) {
|
|
634
684
|
await clearFileGrant();
|
|
635
|
-
await store.deleteRecord(
|
|
685
|
+
await store.deleteRecord(key);
|
|
636
686
|
} else {
|
|
637
|
-
await store.modifyRecord(
|
|
687
|
+
await store.modifyRecord(key, () => Promise.resolve({
|
|
638
688
|
kind: "grant",
|
|
639
|
-
payload: grant
|
|
689
|
+
payload: bind(grant)
|
|
640
690
|
}));
|
|
641
691
|
await clearFileGrantQuietly();
|
|
642
692
|
}
|
|
@@ -649,17 +699,17 @@ function grantStorage(stateDir, credentials) {
|
|
|
649
699
|
if (store !== void 0) {
|
|
650
700
|
await readHostGrant(store);
|
|
651
701
|
let remove = false;
|
|
652
|
-
await store.modifyRecord(
|
|
653
|
-
const result = await change(current?.kind === "grant"
|
|
702
|
+
await store.modifyRecord(key, async (current) => {
|
|
703
|
+
const result = await change(current?.kind === "grant" ? readGrant(current.payload) : void 0);
|
|
654
704
|
if (result.kind === "set") return {
|
|
655
705
|
kind: "grant",
|
|
656
|
-
payload: result.grant
|
|
706
|
+
payload: bind(result.grant)
|
|
657
707
|
};
|
|
658
708
|
remove = result.kind === "delete";
|
|
659
709
|
});
|
|
660
710
|
if (remove) {
|
|
661
711
|
await clearFileGrant();
|
|
662
|
-
await store.deleteRecord(
|
|
712
|
+
await store.deleteRecord(key);
|
|
663
713
|
}
|
|
664
714
|
return;
|
|
665
715
|
}
|
|
@@ -682,6 +732,100 @@ function grantStorage(stateDir, credentials) {
|
|
|
682
732
|
};
|
|
683
733
|
}
|
|
684
734
|
//#endregion
|
|
735
|
+
//#region src/auth/sign-in-state.ts
|
|
736
|
+
const REDEEM_PATH = "/api/v1/vibedev/link/redeem";
|
|
737
|
+
const NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
738
|
+
"ECONNREFUSED",
|
|
739
|
+
"ECONNRESET",
|
|
740
|
+
"ECONNABORTED",
|
|
741
|
+
"ENOTFOUND",
|
|
742
|
+
"EAI_AGAIN",
|
|
743
|
+
"ENETUNREACH",
|
|
744
|
+
"EHOSTUNREACH",
|
|
745
|
+
"EPIPE",
|
|
746
|
+
"ETIMEDOUT",
|
|
747
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
748
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
749
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
750
|
+
"UND_ERR_SOCKET",
|
|
751
|
+
"CERT_HAS_EXPIRED",
|
|
752
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
753
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
754
|
+
"ERR_TLS_CERT_ALTNAME_INVALID"
|
|
755
|
+
]);
|
|
756
|
+
const TIMEOUT_CODES = /* @__PURE__ */ new Set([
|
|
757
|
+
"ETIMEDOUT",
|
|
758
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
759
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
760
|
+
"UND_ERR_BODY_TIMEOUT"
|
|
761
|
+
]);
|
|
762
|
+
/** Read only fixed transport codes, never error text, stack, body or arbitrary fields. */
|
|
763
|
+
function transportFailure(error) {
|
|
764
|
+
let cause = error;
|
|
765
|
+
let timeout = false;
|
|
766
|
+
let networkCode;
|
|
767
|
+
for (let depth = 0; depth < 5 && typeof cause === "object" && cause !== null; depth++) {
|
|
768
|
+
const value = cause;
|
|
769
|
+
if (value.name === "TimeoutError") timeout = true;
|
|
770
|
+
if (typeof value.code === "string" && NETWORK_CODES.has(value.code)) {
|
|
771
|
+
networkCode ??= value.code;
|
|
772
|
+
if (TIMEOUT_CODES.has(value.code)) timeout = true;
|
|
773
|
+
}
|
|
774
|
+
cause = value.cause;
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
errorCode: timeout ? "timeout" : "network",
|
|
778
|
+
...networkCode === void 0 ? {} : { networkCode }
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
const EXPLANATIONS = {
|
|
782
|
+
network: ["无法连接登录网关,请检查网络后重新登录。", "The sign-in gateway could not be reached. Check the connection and start sign-in again."],
|
|
783
|
+
timeout: ["登录等待或网关请求已超时,请重新登录。", "Sign-in or the gateway request timed out. Start sign-in again."],
|
|
784
|
+
"gateway-refused": ["登录网关拒绝了请求,请重新登录。", "The gateway refused the sign-in request. Start sign-in again."],
|
|
785
|
+
"gateway-unavailable": ["登录网关暂时不可用,请稍后重新登录。", "The sign-in gateway is temporarily unavailable. Start sign-in again later."],
|
|
786
|
+
protocol: ["登录网关返回了无效的凭据格式,请稍后重新登录。", "The gateway returned an invalid credential response. Start sign-in again later."],
|
|
787
|
+
storage: ["无法准备或保存本地登录凭据,请检查存储权限后重新登录。", "Local sign-in credentials could not be prepared or saved. Check storage permissions and start sign-in again."],
|
|
788
|
+
callback: ["无法启动或使用本地登录回调,请重新登录。", "The local sign-in callback could not be started or used. Start sign-in again."]
|
|
789
|
+
};
|
|
790
|
+
const STAGES = {
|
|
791
|
+
listen: ["本地回调监听", "Local callback listener"],
|
|
792
|
+
device: ["准备本地设备标识", "Preparing the local device identity"],
|
|
793
|
+
redeem: ["交换登录凭据", "Exchanging sign-in credentials"],
|
|
794
|
+
persist: ["保存本地凭据", "Saving local credentials"]
|
|
795
|
+
};
|
|
796
|
+
/** A fixed bilingual explanation suitable for exposed rejection messages. */
|
|
797
|
+
function failureMessage(attempt) {
|
|
798
|
+
const [chinese, english] = EXPLANATIONS[attempt.errorCode ?? "callback"];
|
|
799
|
+
const stage = attempt.stage === void 0 ? "" : ` ${STAGES[attempt.stage].join(" / ")}; stage=${attempt.stage}.`;
|
|
800
|
+
const status = attempt.httpStatus === void 0 ? "" : ` HTTP ${attempt.httpStatus}.`;
|
|
801
|
+
return `${english} ${chinese} errorCode=${attempt.errorCode ?? "callback"}.${stage}${status}`;
|
|
802
|
+
}
|
|
803
|
+
const escapeHtml = (text) => text.replace(/[&<>"']/g, (character) => ({
|
|
804
|
+
"&": "&",
|
|
805
|
+
"<": "<",
|
|
806
|
+
">": ">",
|
|
807
|
+
"\"": """,
|
|
808
|
+
"'": "'"
|
|
809
|
+
})[character]);
|
|
810
|
+
/** Only our fixed explanation and escaped safe enum/status fields reach the browser. */
|
|
811
|
+
function failurePage(attempt) {
|
|
812
|
+
return `<h1>Sign-in failed.</h1><p>${escapeHtml(failureMessage(attempt))}</p><p>请回到对话重新登录。</p>`;
|
|
813
|
+
}
|
|
814
|
+
/** Explicit log allowlist; adding fields to the public attempt never expands log output. */
|
|
815
|
+
function attemptLog(event, attempt) {
|
|
816
|
+
return `dsh-vibedev sign-in ${JSON.stringify({
|
|
817
|
+
event,
|
|
818
|
+
id: attempt.id,
|
|
819
|
+
phase: attempt.phase,
|
|
820
|
+
gatewayOrigin: attempt.gatewayOrigin,
|
|
821
|
+
path: REDEEM_PATH,
|
|
822
|
+
...attempt.stage === void 0 ? {} : { stage: attempt.stage },
|
|
823
|
+
...attempt.errorCode === void 0 ? {} : { errorCode: attempt.errorCode },
|
|
824
|
+
...attempt.httpStatus === void 0 ? {} : { httpStatus: attempt.httpStatus },
|
|
825
|
+
...attempt.networkCode === void 0 ? {} : { networkCode: attempt.networkCode }
|
|
826
|
+
})}`;
|
|
827
|
+
}
|
|
828
|
+
//#endregion
|
|
685
829
|
//#region src/auth/login.ts
|
|
686
830
|
/**
|
|
687
831
|
* The plugin's own VibeDev sign-in, for hosts whose account service does not
|
|
@@ -768,6 +912,7 @@ var PluginLogin = class {
|
|
|
768
912
|
readFailureReported = false;
|
|
769
913
|
refreshing;
|
|
770
914
|
pending;
|
|
915
|
+
attempt;
|
|
771
916
|
listeners = /* @__PURE__ */ new Set();
|
|
772
917
|
now;
|
|
773
918
|
origin;
|
|
@@ -777,7 +922,7 @@ var PluginLogin = class {
|
|
|
777
922
|
constructor(options) {
|
|
778
923
|
this.options = options;
|
|
779
924
|
this.now = options.now ?? Date.now;
|
|
780
|
-
this.origin = options.origin
|
|
925
|
+
this.origin = normalizeGatewayOrigin(options.origin);
|
|
781
926
|
}
|
|
782
927
|
readStored(notify) {
|
|
783
928
|
if (this.reading !== void 0) return this.reading;
|
|
@@ -812,10 +957,32 @@ var PluginLogin = class {
|
|
|
812
957
|
const changed = this.grant?.accessToken !== grant?.accessToken;
|
|
813
958
|
this.grant = grant;
|
|
814
959
|
if (!changed) return;
|
|
960
|
+
this.notify();
|
|
961
|
+
}
|
|
962
|
+
notify() {
|
|
815
963
|
for (const listener of this.listeners) try {
|
|
816
964
|
listener();
|
|
817
965
|
} catch {}
|
|
818
966
|
}
|
|
967
|
+
/** A copied safe snapshot, retained after completion until the next sign-in. */
|
|
968
|
+
signInAttempt() {
|
|
969
|
+
return this.attempt === void 0 ? void 0 : Object.freeze({ ...this.attempt });
|
|
970
|
+
}
|
|
971
|
+
logAttempt(event) {
|
|
972
|
+
if (this.attempt === void 0) return;
|
|
973
|
+
try {
|
|
974
|
+
this.options.log?.(attemptLog(event, this.attempt));
|
|
975
|
+
} catch {}
|
|
976
|
+
}
|
|
977
|
+
transition(id, change, event = "phase") {
|
|
978
|
+
if (this.attempt?.id !== id) return;
|
|
979
|
+
this.attempt = {
|
|
980
|
+
...this.attempt,
|
|
981
|
+
...change
|
|
982
|
+
};
|
|
983
|
+
this.logAttempt(event);
|
|
984
|
+
this.notify();
|
|
985
|
+
}
|
|
819
986
|
async save(grant) {
|
|
820
987
|
await this.options.storage.write(grant);
|
|
821
988
|
this.adopt(grant);
|
|
@@ -838,9 +1005,10 @@ var PluginLogin = class {
|
|
|
838
1005
|
}
|
|
839
1006
|
/** Whether a sign-in is waiting for the browser, and its link. */
|
|
840
1007
|
pendingSignIn() {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1008
|
+
const pending = this.pending;
|
|
1009
|
+
return pending?.url === void 0 ? void 0 : {
|
|
1010
|
+
url: pending.url,
|
|
1011
|
+
expiresAt: pending.expiresAt
|
|
844
1012
|
};
|
|
845
1013
|
}
|
|
846
1014
|
/**
|
|
@@ -896,7 +1064,7 @@ var PluginLogin = class {
|
|
|
896
1064
|
});
|
|
897
1065
|
const body = await response.json().catch(() => void 0);
|
|
898
1066
|
if (response.status === 401 || response.status === 403) {
|
|
899
|
-
this.options.log?.(`dsh-vibedev: the VibeDev session ended (${
|
|
1067
|
+
this.options.log?.(`dsh-vibedev: the VibeDev session ended (HTTP ${response.status}); signed out`);
|
|
900
1068
|
outcome = void 0;
|
|
901
1069
|
return { kind: "delete" };
|
|
902
1070
|
}
|
|
@@ -923,7 +1091,7 @@ var PluginLogin = class {
|
|
|
923
1091
|
})();
|
|
924
1092
|
return this.refreshing;
|
|
925
1093
|
}
|
|
926
|
-
post(path, json, token) {
|
|
1094
|
+
post(path, json, token, signal = AbortSignal.timeout(3e4)) {
|
|
927
1095
|
return (this.options.fetch ?? globalThis.fetch)(`${this.origin}${path}`, {
|
|
928
1096
|
method: "POST",
|
|
929
1097
|
headers: {
|
|
@@ -933,7 +1101,8 @@ var PluginLogin = class {
|
|
|
933
1101
|
...token === void 0 ? {} : { authorization: `Bearer ${token}` }
|
|
934
1102
|
},
|
|
935
1103
|
body: JSON.stringify(json),
|
|
936
|
-
|
|
1104
|
+
redirect: "error",
|
|
1105
|
+
signal
|
|
937
1106
|
});
|
|
938
1107
|
}
|
|
939
1108
|
/**
|
|
@@ -944,19 +1113,22 @@ var PluginLogin = class {
|
|
|
944
1113
|
* @returns the authorization link, whether a browser was opened, and the completion.
|
|
945
1114
|
*/
|
|
946
1115
|
async startSignIn(request = {}) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1116
|
+
if (this.pending !== void 0 && !this.pending.committing && this.pending.expiresAt <= this.now()) this.pending.cancel("expired");
|
|
1117
|
+
const active = this.pending ?? this.beginSignIn();
|
|
1118
|
+
const started = await active.ready;
|
|
1119
|
+
const opened = request.open === false || this.pending !== active ? false : await (this.options.openBrowser ?? openInBrowser)(started.url).catch(() => false);
|
|
1120
|
+
return {
|
|
1121
|
+
...started,
|
|
1122
|
+
opened
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
beginSignIn() {
|
|
957
1126
|
const verifier = base64url(randomBytes(32));
|
|
958
1127
|
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
959
1128
|
const state = base64url(randomBytes(18));
|
|
1129
|
+
const id = randomUUID();
|
|
1130
|
+
const expiresAt = this.now() + (this.options.signInTimeoutMs ?? 3e5);
|
|
1131
|
+
const controller = new AbortController();
|
|
960
1132
|
let resolveDone;
|
|
961
1133
|
let rejectDone;
|
|
962
1134
|
const done = new Promise((resolve, reject) => {
|
|
@@ -964,8 +1136,73 @@ var PluginLogin = class {
|
|
|
964
1136
|
rejectDone = reject;
|
|
965
1137
|
});
|
|
966
1138
|
done.catch(() => void 0);
|
|
967
|
-
let
|
|
968
|
-
|
|
1139
|
+
let resolveReady;
|
|
1140
|
+
let rejectReady;
|
|
1141
|
+
const ready = new Promise((resolve, reject) => {
|
|
1142
|
+
resolveReady = resolve;
|
|
1143
|
+
rejectReady = reject;
|
|
1144
|
+
});
|
|
1145
|
+
ready.catch(() => void 0);
|
|
1146
|
+
let finished = false;
|
|
1147
|
+
let claimed = false;
|
|
1148
|
+
let server;
|
|
1149
|
+
let timer;
|
|
1150
|
+
const finish = () => {
|
|
1151
|
+
finished = true;
|
|
1152
|
+
clearTimeout(timer);
|
|
1153
|
+
server?.close();
|
|
1154
|
+
server?.closeAllConnections?.();
|
|
1155
|
+
if (this.pending === active) this.pending = void 0;
|
|
1156
|
+
};
|
|
1157
|
+
const fail = (failure, phase = "failed") => {
|
|
1158
|
+
const attempt = {
|
|
1159
|
+
...this.attempt,
|
|
1160
|
+
...failure,
|
|
1161
|
+
phase
|
|
1162
|
+
};
|
|
1163
|
+
const error = new MediaError(phase === "cancelled" ? "The sign-in was cancelled." : failureMessage(attempt), phase === "expired" ? "SIGN_IN_TIMEOUT" : phase === "cancelled" ? "SIGN_IN_CANCELLED" : "SIGN_IN_REFUSED", attempt.httpStatus === void 0 ? {} : { status: attempt.httpStatus });
|
|
1164
|
+
finish();
|
|
1165
|
+
controller.abort();
|
|
1166
|
+
this.transition(id, {
|
|
1167
|
+
...failure,
|
|
1168
|
+
phase
|
|
1169
|
+
}, "terminal");
|
|
1170
|
+
rejectReady(error);
|
|
1171
|
+
rejectDone(error);
|
|
1172
|
+
return error;
|
|
1173
|
+
};
|
|
1174
|
+
const active = {
|
|
1175
|
+
id,
|
|
1176
|
+
expiresAt,
|
|
1177
|
+
ready,
|
|
1178
|
+
done,
|
|
1179
|
+
committing: false,
|
|
1180
|
+
cancel: (phase) => {
|
|
1181
|
+
if (!finished && !active.committing) fail(phase === "expired" ? { errorCode: "timeout" } : {}, phase);
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
this.pending = active;
|
|
1185
|
+
this.attempt = {
|
|
1186
|
+
id,
|
|
1187
|
+
expiresAt,
|
|
1188
|
+
gatewayOrigin: this.origin,
|
|
1189
|
+
phase: "preparing",
|
|
1190
|
+
stage: "listen"
|
|
1191
|
+
};
|
|
1192
|
+
this.logAttempt("request");
|
|
1193
|
+
this.notify();
|
|
1194
|
+
const live = () => {
|
|
1195
|
+
if (finished || this.pending !== active) return false;
|
|
1196
|
+
if (!active.committing && this.now() >= expiresAt) {
|
|
1197
|
+
active.cancel("expired");
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
return true;
|
|
1201
|
+
};
|
|
1202
|
+
if (!live()) return active;
|
|
1203
|
+
timer = setTimeout(() => active.cancel("expired"), Math.max(0, expiresAt - this.now()));
|
|
1204
|
+
timer.unref();
|
|
1205
|
+
const callback = (request, response) => {
|
|
969
1206
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
970
1207
|
if (url.pathname !== CALLBACK_PATH) {
|
|
971
1208
|
response.writeHead(404).end();
|
|
@@ -977,107 +1214,170 @@ var PluginLogin = class {
|
|
|
977
1214
|
"cache-control": "no-store"
|
|
978
1215
|
}).end(PAGE(title, body));
|
|
979
1216
|
};
|
|
980
|
-
if (
|
|
981
|
-
|
|
1217
|
+
if (request.method !== "GET") {
|
|
1218
|
+
response.setHeader("allow", "GET");
|
|
1219
|
+
answer(405, "VibeDev", "<h1>Use GET for sign-in.</h1><p>请使用浏览器登录回调。</p>");
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (claimed || !live()) {
|
|
1223
|
+
answer(410, "VibeDev", "<h1>This sign-in link has already been used.</h1><p>此登录链接已使用过或已失效。</p>");
|
|
982
1224
|
return;
|
|
983
1225
|
}
|
|
984
|
-
|
|
1226
|
+
const states = url.searchParams.getAll("state");
|
|
1227
|
+
const codes = url.searchParams.getAll("code");
|
|
1228
|
+
const code = codes[0];
|
|
1229
|
+
if (states.length !== 1 || states[0] !== state || codes.length !== 1 || code === void 0 || code.trim() === "") {
|
|
985
1230
|
answer(400, "VibeDev", "<h1>Sign-in could not be completed.</h1><p>登录未完成:链接无效,请回到对话重新登录。</p>");
|
|
986
1231
|
return;
|
|
987
1232
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1233
|
+
claimed = true;
|
|
1234
|
+
this.transition(id, {
|
|
1235
|
+
phase: "exchanging",
|
|
1236
|
+
stage: "device"
|
|
1237
|
+
}, "request");
|
|
1238
|
+
(async () => {
|
|
1239
|
+
let grant;
|
|
1240
|
+
try {
|
|
1241
|
+
if (!live()) return;
|
|
1242
|
+
const deviceId = await this.options.storage.deviceId();
|
|
1243
|
+
if (!live()) return;
|
|
1244
|
+
this.transition(id, { stage: "redeem" }, "request");
|
|
1245
|
+
if (!live()) return;
|
|
1246
|
+
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(3e4)]);
|
|
1247
|
+
const response = await this.post(REDEEM_PATH, {
|
|
1248
|
+
code,
|
|
1249
|
+
code_verifier: verifier,
|
|
1250
|
+
token_type: "app",
|
|
1251
|
+
client: this.options.client ?? "vibedev-plugin",
|
|
1252
|
+
device_id: deviceId,
|
|
1253
|
+
device_model: `${platform()}-${arch()}`.slice(0, 128),
|
|
1254
|
+
os_version: release().slice(0, 128)
|
|
1255
|
+
}, void 0, signal);
|
|
1256
|
+
if (!live()) {
|
|
1257
|
+
response.body?.cancel().catch(() => void 0);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
this.transition(id, { httpStatus: response.status }, "response");
|
|
1261
|
+
if (!live()) {
|
|
1262
|
+
response.body?.cancel().catch(() => void 0);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (!response.ok) {
|
|
1266
|
+
response.body?.cancel().catch(() => void 0);
|
|
1267
|
+
const errorCode = response.status === 429 || response.status >= 500 ? "gateway-unavailable" : response.status >= 400 && response.status < 500 ? "gateway-refused" : "protocol";
|
|
1268
|
+
answer(502, "VibeDev", failurePage({
|
|
1269
|
+
...this.attempt,
|
|
1270
|
+
errorCode
|
|
1271
|
+
}));
|
|
1272
|
+
fail({ errorCode });
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
let body;
|
|
1276
|
+
try {
|
|
1277
|
+
body = await response.json();
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
if (!live()) return;
|
|
1280
|
+
const transport = transportFailure(error);
|
|
1281
|
+
const failure = transport.networkCode !== void 0 || transport.errorCode === "timeout" ? transport : { errorCode: "protocol" };
|
|
1282
|
+
answer(502, "VibeDev", failurePage({
|
|
1283
|
+
...this.attempt,
|
|
1284
|
+
...failure
|
|
1285
|
+
}));
|
|
1286
|
+
fail(failure);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (!live()) return;
|
|
1290
|
+
const envelope = record$6(body);
|
|
1291
|
+
const parsed = envelope !== void 0 && (envelope.code === void 0 || envelope.code === 0) && (envelope.data === void 0 || record$6(envelope.data) !== void 0) ? parseGrant(body, this.now()) : void 0;
|
|
1292
|
+
if (parsed === void 0) {
|
|
1293
|
+
answer(502, "VibeDev", failurePage({
|
|
1294
|
+
...this.attempt,
|
|
1295
|
+
errorCode: "protocol"
|
|
1296
|
+
}));
|
|
1297
|
+
fail({ errorCode: "protocol" });
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
grant = parsed;
|
|
1301
|
+
active.committing = true;
|
|
1302
|
+
clearTimeout(timer);
|
|
1303
|
+
this.transition(id, {
|
|
1304
|
+
phase: "committing",
|
|
1305
|
+
stage: "persist"
|
|
1306
|
+
});
|
|
1307
|
+
await this.save(grant);
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
if (finished || this.pending !== active) return;
|
|
1310
|
+
const failure = this.attempt?.stage === "device" || this.attempt?.stage === "persist" ? { errorCode: "storage" } : transportFailure(error);
|
|
1311
|
+
answer(502, "VibeDev", failurePage({
|
|
1312
|
+
...this.attempt,
|
|
1313
|
+
...failure
|
|
1314
|
+
}));
|
|
1315
|
+
fail(failure);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
answer(200, "VibeDev", "<h1>Signed in to VibeDev.</h1><p>已登录 VibeDev,可以关闭此页面,回到对话继续。</p><p>You can close this tab and return to the conversation.</p>");
|
|
993
1319
|
finish();
|
|
1320
|
+
this.transition(id, { phase: "succeeded" }, "terminal");
|
|
994
1321
|
resolveDone(grant);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1322
|
+
})();
|
|
1323
|
+
};
|
|
1324
|
+
try {
|
|
1325
|
+
server = createServer(callback);
|
|
1326
|
+
server.on("error", () => {
|
|
1327
|
+
if (finished || active.committing) return;
|
|
1328
|
+
this.transition(id, { stage: "listen" });
|
|
1329
|
+
if (!finished) fail({ errorCode: "callback" });
|
|
999
1330
|
});
|
|
1000
|
-
});
|
|
1001
|
-
await new Promise((resolve, reject) => {
|
|
1002
|
-
server.once("error", reject);
|
|
1003
1331
|
server.listen(0, "127.0.0.1", () => {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const callback = `http://127.0.0.1:${server.address().port}${CALLBACK_PATH}`;
|
|
1009
|
-
const url = `${this.origin}/vibedev-link?${new URLSearchParams({
|
|
1010
|
-
callback,
|
|
1011
|
-
state,
|
|
1012
|
-
response_type: "code",
|
|
1013
|
-
code_challenge: challenge,
|
|
1014
|
-
code_challenge_method: "S256",
|
|
1015
|
-
client: this.options.client ?? "vibedev-plugin"
|
|
1016
|
-
}).toString()}`;
|
|
1017
|
-
const timeoutMs = this.options.signInTimeoutMs ?? 3e5;
|
|
1018
|
-
const timer = setTimeout(() => {
|
|
1019
|
-
if (settled) return;
|
|
1020
|
-
settled = true;
|
|
1021
|
-
finish();
|
|
1022
|
-
rejectDone(new MediaError("The VibeDev sign-in was not completed in time.", "SIGN_IN_TIMEOUT"));
|
|
1023
|
-
}, timeoutMs);
|
|
1024
|
-
timer.unref();
|
|
1025
|
-
const finish = () => {
|
|
1026
|
-
clearTimeout(timer);
|
|
1027
|
-
server.close();
|
|
1028
|
-
server.closeAllConnections?.();
|
|
1029
|
-
if (this.pending?.server === server) this.pending = void 0;
|
|
1030
|
-
};
|
|
1031
|
-
this.pending = {
|
|
1032
|
-
url,
|
|
1033
|
-
expiresAt: this.now() + timeoutMs,
|
|
1034
|
-
done,
|
|
1035
|
-
server,
|
|
1036
|
-
cancel: (reason) => {
|
|
1037
|
-
if (!settled) {
|
|
1038
|
-
settled = true;
|
|
1039
|
-
finish();
|
|
1040
|
-
rejectDone(reason);
|
|
1332
|
+
if (!live()) {
|
|
1333
|
+
server?.close();
|
|
1334
|
+
server?.closeAllConnections?.();
|
|
1335
|
+
return;
|
|
1041
1336
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
await this.save(grant);
|
|
1067
|
-
return grant;
|
|
1337
|
+
const port = (server?.address()).port;
|
|
1338
|
+
const url = `${this.origin}/vibedev-link?${new URLSearchParams({
|
|
1339
|
+
callback: `http://127.0.0.1:${port}${CALLBACK_PATH}`,
|
|
1340
|
+
state,
|
|
1341
|
+
response_type: "code",
|
|
1342
|
+
code_challenge: challenge,
|
|
1343
|
+
code_challenge_method: "S256",
|
|
1344
|
+
client: this.options.client ?? "vibedev-plugin"
|
|
1345
|
+
}).toString()}`;
|
|
1346
|
+
active.url = url;
|
|
1347
|
+
this.transition(id, {
|
|
1348
|
+
phase: "waiting-browser",
|
|
1349
|
+
stage: void 0
|
|
1350
|
+
});
|
|
1351
|
+
if (live()) resolveReady({
|
|
1352
|
+
url,
|
|
1353
|
+
expiresAt,
|
|
1354
|
+
done
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
} catch {
|
|
1358
|
+
if (!finished) fail({ errorCode: "callback" });
|
|
1359
|
+
}
|
|
1360
|
+
return active;
|
|
1068
1361
|
}
|
|
1069
1362
|
/** Sign out: revoke the session at the gateway (best effort) and forget it. */
|
|
1070
1363
|
async signOut() {
|
|
1364
|
+
const attemptId = this.attempt?.id;
|
|
1365
|
+
const pending = this.pending;
|
|
1366
|
+
if (pending?.committing) await pending.done.catch(() => void 0);
|
|
1367
|
+
else pending?.cancel("cancelled");
|
|
1071
1368
|
await this.load();
|
|
1072
1369
|
const grant = this.grant;
|
|
1073
|
-
|
|
1370
|
+
if (grant !== void 0) await this.save(void 0);
|
|
1371
|
+
if (this.attempt?.id === attemptId && this.pending === void 0) {
|
|
1372
|
+
this.attempt = void 0;
|
|
1373
|
+
this.notify();
|
|
1374
|
+
}
|
|
1074
1375
|
if (grant === void 0) return;
|
|
1075
|
-
await this.save(void 0);
|
|
1076
1376
|
await this.post("/api/v1/vibedev/app-token/revoke", { refresh_token: grant.refreshToken }, grant.accessToken).then((response) => response.body?.cancel(), () => void 0);
|
|
1077
1377
|
}
|
|
1078
1378
|
/** Stop a waiting sign-in. */
|
|
1079
1379
|
dispose() {
|
|
1080
|
-
this.pending?.cancel(
|
|
1380
|
+
this.pending?.cancel("cancelled");
|
|
1081
1381
|
}
|
|
1082
1382
|
};
|
|
1083
1383
|
//#endregion
|
|
@@ -2491,7 +2791,12 @@ var GatewayAdapter = class extends LlmAdapter {
|
|
|
2491
2791
|
const token = credential.token;
|
|
2492
2792
|
let response;
|
|
2493
2793
|
const observingFetch = async (input, init) => {
|
|
2494
|
-
const
|
|
2794
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
2795
|
+
if (new URL(url).origin !== snapshot.origin) throw new Error("Refusing a model request to another gateway origin.");
|
|
2796
|
+
const received = await fetch(input, {
|
|
2797
|
+
...init,
|
|
2798
|
+
redirect: "error"
|
|
2799
|
+
});
|
|
2495
2800
|
response = {
|
|
2496
2801
|
status: received.status,
|
|
2497
2802
|
headers: Object.fromEntries(received.headers.entries())
|
|
@@ -3137,7 +3442,7 @@ var GatewayHttp = class {
|
|
|
3137
3442
|
method: request.method ?? (body === void 0 ? "GET" : "POST"),
|
|
3138
3443
|
headers,
|
|
3139
3444
|
...body === void 0 ? {} : { body },
|
|
3140
|
-
redirect: "
|
|
3445
|
+
redirect: "error",
|
|
3141
3446
|
signal
|
|
3142
3447
|
});
|
|
3143
3448
|
} catch (error) {
|
|
@@ -3345,6 +3650,8 @@ function parseVideoCapabilities(raw) {
|
|
|
3345
3650
|
["maxAssetBytes", "max_asset_bytes"],
|
|
3346
3651
|
["minReferenceVideoSeconds", "min_reference_video_duration_seconds"],
|
|
3347
3652
|
["maxReferenceVideoSeconds", "max_reference_video_duration_seconds"],
|
|
3653
|
+
["minReferenceAudioSeconds", "min_reference_audio_duration_seconds"],
|
|
3654
|
+
["maxReferenceAudioSeconds", "max_reference_audio_duration_seconds"],
|
|
3348
3655
|
["maxTotalReferenceVideoSeconds", "max_total_reference_video_duration_seconds"]
|
|
3349
3656
|
]) {
|
|
3350
3657
|
const value = positive(field(raw, snake));
|
|
@@ -4524,7 +4831,7 @@ function startTaskJob(runtime, exec, record) {
|
|
|
4524
4831
|
/**
|
|
4525
4832
|
* Submit a task and start its report. An unconfirmed submission is not an
|
|
4526
4833
|
* error here: the agent must not resubmit it, so it is reported like any
|
|
4527
|
-
* other task while the tracker
|
|
4834
|
+
* other task while the tracker settles that one submission under the same key.
|
|
4528
4835
|
* @param runtime - the media runtime.
|
|
4529
4836
|
* @param exec - the tool call.
|
|
4530
4837
|
* @param draft - the task to submit.
|
|
@@ -4548,7 +4855,7 @@ async function submitTask(runtime, exec, draft) {
|
|
|
4548
4855
|
};
|
|
4549
4856
|
}
|
|
4550
4857
|
/** The sentence about an unconfirmed submission. */
|
|
4551
|
-
const UNCONFIRMED_NOTE = "The gateway did not confirm the submission; dsh-vibedev keeps
|
|
4858
|
+
const UNCONFIRMED_NOTE = "The gateway did not confirm the submission; dsh-vibedev keeps settling that same submission under the same request key for a few minutes, so no second task can be created from it. Do not submit it again.";
|
|
4552
4859
|
/**
|
|
4553
4860
|
* Wait for a task in the tool call itself, for hosts without background jobs.
|
|
4554
4861
|
* @param runtime - the media runtime.
|
|
@@ -4585,7 +4892,7 @@ const text$2 = (value) => typeof value === "string" && value.trim() !== "" ? val
|
|
|
4585
4892
|
const noPaths = { readPath: () => Promise.reject(new MediaError("The gateway returned an unexpected image reference.", "GATEWAY_BAD_RESPONSE")) };
|
|
4586
4893
|
/**
|
|
4587
4894
|
* Read the images of an `/v1/images/*` answer: `b64_json`, a `data:` URL, or a link.
|
|
4588
|
-
* @param runtime - for
|
|
4895
|
+
* @param runtime - for downloading the gateway's full signed result URLs.
|
|
4589
4896
|
* @param body - the parsed answer.
|
|
4590
4897
|
* @param signal - cancels downloads.
|
|
4591
4898
|
* @returns the images and the revised prompt, when the model returned one.
|
|
@@ -4605,7 +4912,7 @@ async function decodeImages(runtime, body, signal) {
|
|
|
4605
4912
|
else if (url?.startsWith("data:") === true) data = (await loadMedia(url, noPaths, MAX_OUTPUT_BYTES)).data;
|
|
4606
4913
|
else if (url !== void 0 && /^https?:\/\//i.test(url)) {
|
|
4607
4914
|
const response = await runtime.http.send(url, {
|
|
4608
|
-
anonymous:
|
|
4915
|
+
anonymous: true,
|
|
4609
4916
|
headers: { accept: "*/*" },
|
|
4610
4917
|
timeoutMs: 12e4,
|
|
4611
4918
|
signal
|
|
@@ -4934,6 +5241,7 @@ function describeVideoModel(model) {
|
|
|
4934
5241
|
const limits = [];
|
|
4935
5242
|
if (video.minReferenceVideoSeconds !== void 0 || video.maxReferenceVideoSeconds !== void 0) limits.push(`each reference video ${video.minReferenceVideoSeconds ?? 0}-${video.maxReferenceVideoSeconds ?? "∞"} s`);
|
|
4936
5243
|
if (video.maxTotalReferenceVideoSeconds !== void 0) limits.push(`reference videos at most ${video.maxTotalReferenceVideoSeconds} s in total`);
|
|
5244
|
+
if (video.minReferenceAudioSeconds !== void 0 || video.maxReferenceAudioSeconds !== void 0) limits.push(`each reference audio ${video.minReferenceAudioSeconds ?? 0}-${video.maxReferenceAudioSeconds ?? "∞"} s`);
|
|
4937
5245
|
if (video.combinations !== void 0) limits.push(`allowed combinations: ${video.combinations.map((c) => [
|
|
4938
5246
|
c.duration === void 0 ? "" : `${c.duration}s`,
|
|
4939
5247
|
c.ratio ?? "",
|
|
@@ -5091,12 +5399,15 @@ function referenceByteLimit(model, input) {
|
|
|
5091
5399
|
return Math.min(kindMax ?? Infinity, video.maxAssetBytes ?? Infinity, MEDIA_LIBRARY_MAX_BYTES);
|
|
5092
5400
|
}
|
|
5093
5401
|
/**
|
|
5094
|
-
* Check reference
|
|
5402
|
+
* Check the measured lengths of the reference media once the gateway has
|
|
5403
|
+
* measured them. A reference's OWN length is what is checked — never the
|
|
5404
|
+
* requested output duration — and only against the limits that model declares.
|
|
5095
5405
|
* @param model - the video model.
|
|
5096
5406
|
* @param videos - the uploaded reference videos, in order.
|
|
5407
|
+
* @param audios - the uploaded reference audios, in order.
|
|
5097
5408
|
* @throws {@link MediaError} when a clip or the total is outside the model's limits.
|
|
5098
5409
|
*/
|
|
5099
|
-
function checkReferenceDurations(model, videos) {
|
|
5410
|
+
function checkReferenceDurations(model, videos, audios = []) {
|
|
5100
5411
|
const video = model.video ?? {};
|
|
5101
5412
|
let totalMs = 0;
|
|
5102
5413
|
for (const [index, clip] of videos.entries()) {
|
|
@@ -5110,6 +5421,16 @@ function checkReferenceDurations(model, videos) {
|
|
|
5110
5421
|
totalMs += clip.durationMs;
|
|
5111
5422
|
}
|
|
5112
5423
|
if (video.maxTotalReferenceVideoSeconds !== void 0 && totalMs / 1e3 > video.maxTotalReferenceVideoSeconds) throw new MediaError(`The reference videos add up to ${(totalMs / 1e3).toFixed(2)} s; ${model.id} takes at most ${video.maxTotalReferenceVideoSeconds} s in total.`, "REFERENCE_TOTAL_TOO_LONG", { field: "reference_videos" });
|
|
5424
|
+
const audioLimits = video.minReferenceAudioSeconds !== void 0 || video.maxReferenceAudioSeconds !== void 0;
|
|
5425
|
+
for (const [index, clip] of audios.entries()) {
|
|
5426
|
+
if (clip.durationMs === void 0) {
|
|
5427
|
+
if (audioLimits) throw new MediaError(`The gateway could not measure the length of reference audio ${index + 1} ("${clip.source}"), so ${model.id}'s length limits cannot be checked. Export it as a standard MP3 or WAV and try again.`, "REFERENCE_DURATION_UNKNOWN", { field: "reference_audios" });
|
|
5428
|
+
continue;
|
|
5429
|
+
}
|
|
5430
|
+
const seconds = clip.durationMs / 1e3;
|
|
5431
|
+
if (video.minReferenceAudioSeconds !== void 0 && seconds < video.minReferenceAudioSeconds) throw new MediaError(`Reference audio ${index + 1} ("${clip.source}") is ${seconds.toFixed(2)} s; ${model.id} needs at least ${video.minReferenceAudioSeconds} s of reference audio.`, "REFERENCE_AUDIO_TOO_SHORT", { field: "reference_audios" });
|
|
5432
|
+
if (video.maxReferenceAudioSeconds !== void 0 && seconds > video.maxReferenceAudioSeconds) throw new MediaError(`Reference audio ${index + 1} ("${clip.source}") is ${seconds.toFixed(2)} s; ${model.id} takes at most ${video.maxReferenceAudioSeconds} s of reference audio per clip. Trim it first, or use a shorter excerpt.`, "REFERENCE_AUDIO_TOO_LONG", { field: "reference_audios" });
|
|
5433
|
+
}
|
|
5113
5434
|
}
|
|
5114
5435
|
/**
|
|
5115
5436
|
* The `/v1/videos` body for a plan whose inputs are uploaded. The gateway reads
|
|
@@ -5260,7 +5581,12 @@ async function prepareVideoTask(runtime, call, request, save) {
|
|
|
5260
5581
|
bytes: item.media.data.byteLength,
|
|
5261
5582
|
...assets[index]?.durationMs === void 0 ? {} : { durationMs: assets[index]?.durationMs }
|
|
5262
5583
|
}] : []);
|
|
5263
|
-
checkReferenceDurations(model, videos)
|
|
5584
|
+
checkReferenceDurations(model, videos, loaded.flatMap((item, index) => item.input === "referenceAudios" ? [{
|
|
5585
|
+
source: item.media.source,
|
|
5586
|
+
mime: item.media.mime,
|
|
5587
|
+
bytes: item.media.data.byteLength,
|
|
5588
|
+
...assets[index]?.durationMs === void 0 ? {} : { durationMs: assets[index]?.durationMs }
|
|
5589
|
+
}] : []));
|
|
5264
5590
|
const urls = {};
|
|
5265
5591
|
for (const [index, item] of loaded.entries()) {
|
|
5266
5592
|
const asset = assets[index];
|
|
@@ -5898,13 +6224,21 @@ function parseAudioTask(body) {
|
|
|
5898
6224
|
* background job, so a task keeps being followed after its job is stopped and
|
|
5899
6225
|
* after the host restarts.
|
|
5900
6226
|
*
|
|
5901
|
-
* Money rules
|
|
5902
|
-
*
|
|
5903
|
-
*
|
|
5904
|
-
* and
|
|
5905
|
-
*
|
|
5906
|
-
*
|
|
5907
|
-
*
|
|
6227
|
+
* Money rules, and the rule that one operation creates at most one task:
|
|
6228
|
+
*
|
|
6229
|
+
* - A submission is settled by the gateway's structured verdict
|
|
6230
|
+
* (`submission_state` and `retry_action`) first, and only by the HTTP status
|
|
6231
|
+
* when it sends none. `rejected`/`retry_action: none` is final: the reason is
|
|
6232
|
+
* saved and nothing is sent again — not by the tool, not by the background
|
|
6233
|
+
* loop, not after a restart. `retryable` on its own never authorizes creating
|
|
6234
|
+
* another task.
|
|
6235
|
+
* - An unknown outcome keeps the record and settles it under the SAME key and
|
|
6236
|
+
* the SAME body, which the gateway answers with the original task when there
|
|
6237
|
+
* is one. A new key is minted only for a new request the user asks for.
|
|
6238
|
+
* - Once a gateway task id is known the task is only polled; a failed poll
|
|
6239
|
+
* never submits anything.
|
|
6240
|
+
* - A refusal that left nothing under the key ends the record as failed with
|
|
6241
|
+
* the gateway's own reason, so the person sees why.
|
|
5908
6242
|
* @module dsh-vibedev/tasks/tracker
|
|
5909
6243
|
*/
|
|
5910
6244
|
const SPECS = {
|
|
@@ -5945,34 +6279,74 @@ const SUBMIT_TIMEOUT_MS = 3e5;
|
|
|
5945
6279
|
const SUBMISSION_AMBIGUOUS = "VIDEO_SUBMISSION_AMBIGUOUS";
|
|
5946
6280
|
/** The same key was sent with a different body: a plugin bug, never retried. */
|
|
5947
6281
|
const KEY_CONFLICT = /_IDEMPOTENCY_CONFLICT$/;
|
|
6282
|
+
/** Structured verdicts the gateway sends; an unknown word falls back to the older protocol. */
|
|
6283
|
+
const REJECTED_STATES = /* @__PURE__ */ new Set([
|
|
6284
|
+
"rejected",
|
|
6285
|
+
"refused",
|
|
6286
|
+
"denied"
|
|
6287
|
+
]);
|
|
6288
|
+
const RECONCILE_STATES = /* @__PURE__ */ new Set(["unknown", "accepted"]);
|
|
6289
|
+
/** `retry_action` values that keep the same key: a lookup of this submission, never a new one. */
|
|
6290
|
+
const SAME_KEY_ACTIONS = /* @__PURE__ */ new Set([
|
|
6291
|
+
"reconcile",
|
|
6292
|
+
"retry_same_key",
|
|
6293
|
+
"retry_same"
|
|
6294
|
+
]);
|
|
5948
6295
|
/**
|
|
5949
|
-
*
|
|
5950
|
-
*
|
|
5951
|
-
*
|
|
6296
|
+
* What a failed submission means.
|
|
6297
|
+
*
|
|
6298
|
+
* The two STOP rules are read before anything that would keep the operation
|
|
6299
|
+
* alive, so no verdict can talk the plugin into sending a refused request
|
|
6300
|
+
* again: `retry_action: none` (the provider refused it) and
|
|
6301
|
+
* `VIDEO_SUBMISSION_AMBIGUOUS` (the gateway's key answers this forever) end the
|
|
6302
|
+
* operation even when the same answer also says the outcome is unknown.
|
|
6303
|
+
*
|
|
6304
|
+
* After those, the gateway's structured verdict decides, and only when it
|
|
6305
|
+
* sends none do the HTTP status and the older codes. `retryable` says recovery
|
|
6306
|
+
* is still needed; it never authorizes creating another task.
|
|
5952
6307
|
* @param error - the failure.
|
|
5953
|
-
* @returns
|
|
6308
|
+
* @returns how it may be taken further.
|
|
5954
6309
|
*/
|
|
5955
|
-
function
|
|
5956
|
-
if (!(error instanceof MediaError)) return
|
|
5957
|
-
|
|
5958
|
-
if (
|
|
5959
|
-
|
|
5960
|
-
|
|
6310
|
+
function submissionRecovery(error) {
|
|
6311
|
+
if (!(error instanceof MediaError)) return "rejected";
|
|
6312
|
+
const { submissionState, retryAction, status, busy, retryable } = error.details;
|
|
6313
|
+
if (retryAction === "none") return "rejected";
|
|
6314
|
+
if (error.code === SUBMISSION_AMBIGUOUS || KEY_CONFLICT.test(error.code)) return "rejected";
|
|
6315
|
+
if (submissionState !== void 0 && REJECTED_STATES.has(submissionState)) return "rejected";
|
|
6316
|
+
if (retryAction !== void 0 && SAME_KEY_ACTIONS.has(retryAction)) return "reconcile";
|
|
6317
|
+
if (retryAction === "retry") return "wait";
|
|
6318
|
+
if (submissionState !== void 0 && RECONCILE_STATES.has(submissionState)) return "reconcile";
|
|
6319
|
+
if (NO_ANSWER_CODES.has(error.code)) return "reconcile";
|
|
6320
|
+
if (busy === true) return "wait";
|
|
6321
|
+
if (status !== void 0 && status >= 400 && status < 500) return status === 409 ? "reconcile" : "rejected";
|
|
6322
|
+
if (status !== void 0 && status >= 500) return retryable === false ? "rejected" : "reconcile";
|
|
6323
|
+
return "rejected";
|
|
5961
6324
|
}
|
|
5962
6325
|
/**
|
|
5963
|
-
* The record
|
|
6326
|
+
* The failure to record when a submission is refused for good.
|
|
6327
|
+
*
|
|
6328
|
+
* None of these tells the person to generate again: in the two cases where the
|
|
6329
|
+
* provider may already hold the request, a new generation would be a second
|
|
6330
|
+
* paid task, so the wording asks them to check the records first.
|
|
5964
6331
|
* @param error - the failure.
|
|
5965
|
-
* @returns the
|
|
6332
|
+
* @returns the code and message to keep with the task.
|
|
5966
6333
|
*/
|
|
5967
|
-
function
|
|
5968
|
-
if (!(error instanceof MediaError)) return
|
|
6334
|
+
function terminalFailure(error) {
|
|
6335
|
+
if (!(error instanceof MediaError)) return {
|
|
6336
|
+
code: "SUBMISSION_REJECTED",
|
|
6337
|
+
message: messageOf(error)
|
|
6338
|
+
};
|
|
5969
6339
|
if (error.code === SUBMISSION_AMBIGUOUS) return {
|
|
5970
6340
|
code: error.code,
|
|
5971
|
-
message: "The gateway could not confirm
|
|
6341
|
+
message: "The gateway could not confirm this submission with the provider, so it created no task and charged nothing, and this request key will answer the same way. If the provider did receive it, that task is not visible here. Check the task list and the account usage before generating the same thing again."
|
|
5972
6342
|
};
|
|
5973
6343
|
if (KEY_CONFLICT.test(error.code)) return {
|
|
5974
6344
|
code: error.code,
|
|
5975
|
-
message: "The gateway refused the request: its key was already used for a different request. Nothing was charged
|
|
6345
|
+
message: "The gateway refused the request: its key was already used for a different request. Nothing was charged. Check the task list for the earlier request before generating again."
|
|
6346
|
+
};
|
|
6347
|
+
return {
|
|
6348
|
+
code: error.code,
|
|
6349
|
+
message: error.message
|
|
5976
6350
|
};
|
|
5977
6351
|
}
|
|
5978
6352
|
function sleepFor(ms, signal) {
|
|
@@ -6085,8 +6459,14 @@ var TaskTracker = class {
|
|
|
6085
6459
|
await this.releaseClaim(draft.id);
|
|
6086
6460
|
throw error;
|
|
6087
6461
|
}
|
|
6088
|
-
const
|
|
6089
|
-
if (
|
|
6462
|
+
const recovery = submissionRecovery(error);
|
|
6463
|
+
if (recovery === "rejected") {
|
|
6464
|
+
const settled = terminalFailure(error);
|
|
6465
|
+
if (!unknown) {
|
|
6466
|
+
this.dropClaim(draft.id);
|
|
6467
|
+
await this.options.store.remove(draft.id);
|
|
6468
|
+
throw error;
|
|
6469
|
+
}
|
|
6090
6470
|
await this.finish(draft.id, {
|
|
6091
6471
|
status: "failed",
|
|
6092
6472
|
error: settled
|
|
@@ -6094,14 +6474,10 @@ var TaskTracker = class {
|
|
|
6094
6474
|
await this.releaseClaim(draft.id);
|
|
6095
6475
|
throw new MediaError(settled.message, settled.code, { retryable: false }, { cause: error });
|
|
6096
6476
|
}
|
|
6097
|
-
if (!
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
throw error;
|
|
6102
|
-
}
|
|
6103
|
-
lastError = error;
|
|
6104
|
-
break;
|
|
6477
|
+
if (!unknown && recovery === "wait") {
|
|
6478
|
+
this.dropClaim(draft.id);
|
|
6479
|
+
await this.options.store.remove(draft.id);
|
|
6480
|
+
throw error;
|
|
6105
6481
|
}
|
|
6106
6482
|
unknown = true;
|
|
6107
6483
|
lastError = error;
|
|
@@ -6129,7 +6505,7 @@ var TaskTracker = class {
|
|
|
6129
6505
|
}
|
|
6130
6506
|
}
|
|
6131
6507
|
this.followQuietly(draft.id);
|
|
6132
|
-
throw new MediaError(`The VibeDev gateway did not confirm the request (${messageOf(lastError)}). dsh-vibedev keeps
|
|
6508
|
+
throw new MediaError(`The VibeDev gateway did not confirm the request (${messageOf(lastError)}). dsh-vibedev keeps settling it under the same request key for a few minutes — that key can only ever answer with the one task it may have created — and saves the result if it runs. Check media_tasks for task ${draft.id}; do not submit the same request again.`, "SUBMISSION_UNCONFIRMED", { retryable: false });
|
|
6133
6509
|
}
|
|
6134
6510
|
/**
|
|
6135
6511
|
* Follow a recorded task until it ends or the tracker is disposed. A task
|
|
@@ -6295,24 +6671,25 @@ var TaskTracker = class {
|
|
|
6295
6671
|
await this.update(record.id, { progress: "waiting for a VibeDev sign-in" });
|
|
6296
6672
|
return 6e4;
|
|
6297
6673
|
}
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
}
|
|
6314
|
-
|
|
6315
|
-
|
|
6674
|
+
if (submitting) {
|
|
6675
|
+
if (submissionRecovery(error) === "rejected") {
|
|
6676
|
+
await this.finish(record.id, {
|
|
6677
|
+
status: "failed",
|
|
6678
|
+
error: terminalFailure(error)
|
|
6679
|
+
});
|
|
6680
|
+
return;
|
|
6681
|
+
}
|
|
6682
|
+
if (failures >= CONSECUTIVE_FAILURE_LIMIT) {
|
|
6683
|
+
await this.finish(record.id, {
|
|
6684
|
+
status: "failed",
|
|
6685
|
+
error: {
|
|
6686
|
+
code: code ?? "SUBMISSION_FAILED",
|
|
6687
|
+
message: `The submission could not be settled ${failures} times in a row: ${messageOf(error)}`
|
|
6688
|
+
}
|
|
6689
|
+
});
|
|
6690
|
+
return;
|
|
6691
|
+
}
|
|
6692
|
+
return Math.min(6e4, 5e3 * 2 ** Math.min(failures - 1, 4));
|
|
6316
6693
|
}
|
|
6317
6694
|
if (status === 402) {
|
|
6318
6695
|
await this.update(record.id, { progress: "paused: the VibeDev balance is used up; top up to receive the result" });
|
|
@@ -6440,19 +6817,15 @@ var TaskTracker = class {
|
|
|
6440
6817
|
missing
|
|
6441
6818
|
};
|
|
6442
6819
|
}
|
|
6443
|
-
/**
|
|
6820
|
+
/** Use a returned signed URL verbatim and without the app token; old gateways without a URL use `/content`. */
|
|
6444
6821
|
async fetchOutput(record, task, output, temporary) {
|
|
6445
6822
|
const spec = SPECS[record.kind];
|
|
6446
6823
|
const sources = [];
|
|
6447
|
-
if (record.kind === "video") sources.push({
|
|
6448
|
-
url: spec.content(task.id, output.index),
|
|
6449
|
-
anonymous: false
|
|
6450
|
-
});
|
|
6451
6824
|
if (output.url !== void 0) sources.push({
|
|
6452
6825
|
url: output.url,
|
|
6453
6826
|
anonymous: true
|
|
6454
6827
|
});
|
|
6455
|
-
|
|
6828
|
+
else sources.push({
|
|
6456
6829
|
url: spec.content(task.id, output.index),
|
|
6457
6830
|
anonymous: false
|
|
6458
6831
|
});
|
|
@@ -6485,8 +6858,6 @@ var TaskTracker = class {
|
|
|
6485
6858
|
* Settings → VibeDev 账号; this is the agent's way to the same sign-in.
|
|
6486
6859
|
* @module dsh-vibedev/tools/account
|
|
6487
6860
|
*/
|
|
6488
|
-
/** Where new users register. */
|
|
6489
|
-
const REGISTER_URL = "https://vibedev.jzsaas.com";
|
|
6490
6861
|
/**
|
|
6491
6862
|
* Build the tool.
|
|
6492
6863
|
* @param runtime - the media runtime.
|
|
@@ -6567,7 +6938,7 @@ function mediaAccountTool(runtime, chain, login) {
|
|
|
6567
6938
|
return {
|
|
6568
6939
|
source,
|
|
6569
6940
|
signInUrl: started.url,
|
|
6570
|
-
message: `${started.opened ? "Opened the VibeDev sign-in page in the user's browser." : "Could not open a browser."} If it did not appear, give the user this link: ${started.url} — new users can register on that page (or at ${
|
|
6941
|
+
message: `${started.opened ? "Opened the VibeDev sign-in page in the user's browser." : "Could not open a browser."} If it did not appear, give the user this link: ${started.url} — new users can register on that page (or at ${runtime.http.origin}). The sign-in completes in the background within ${minutes} minutes; once the user says they are done, retry the request.`
|
|
6571
6942
|
};
|
|
6572
6943
|
}
|
|
6573
6944
|
});
|
|
@@ -7187,10 +7558,10 @@ const Config = Schema.object({
|
|
|
7187
7558
|
musicModel: Schema.string().default("").volatile(),
|
|
7188
7559
|
podcastModel: Schema.string().default("").volatile(),
|
|
7189
7560
|
transcriptionModel: Schema.string().default("").volatile(),
|
|
7190
|
-
gatewayOrigin: Schema.string().default(
|
|
7561
|
+
gatewayOrigin: Schema.string().default(DEFAULT_GATEWAY_ORIGIN),
|
|
7191
7562
|
client: Schema.string().default("vibedev-plugin"),
|
|
7192
7563
|
primary: Schema.boolean().default(false),
|
|
7193
|
-
apiKeyEnv: Schema.string().default("
|
|
7564
|
+
apiKeyEnv: Schema.string().default(""),
|
|
7194
7565
|
stateDir: Schema.string().default("")
|
|
7195
7566
|
}).i18n({
|
|
7196
7567
|
"zh-CN": {
|
|
@@ -7232,13 +7603,13 @@ const Config = Schema.object({
|
|
|
7232
7603
|
});
|
|
7233
7604
|
const GUIDANCE = "VibeDev media tools: image_generate, video_generate, audio_generate (music or a podcast) and audio_transcribe create and transcribe media with the user's VibeDev account; media_models lists models and prices. Use them on your own judgment whenever generated media serves the request (illustrations, product shots, storyboard frames, short clips, background music, a podcast episode), not only when a tool is named. They spend the user's balance, so make what the request needs rather than speculative variations. Results are saved in the workspace under media/. Pass files the user attached in this chat as chat:1 (the most recent), chat:2, and so on. Video and audio run as background jobs: carry on and let the job notification report the result.";
|
|
7234
7605
|
function apply(ctx, config) {
|
|
7235
|
-
const origin = config.gatewayOrigin
|
|
7606
|
+
const origin = normalizeGatewayOrigin(config.gatewayOrigin);
|
|
7236
7607
|
const stateDir = stateDirectory(config.stateDir);
|
|
7237
7608
|
const userAgent = `vibedev-plugin/${version}`;
|
|
7238
7609
|
const warn = (message) => {
|
|
7239
7610
|
ctx.logger.warn(message);
|
|
7240
7611
|
};
|
|
7241
|
-
const storage = grantStorage(stateDir, () => ctx.get("credentials"));
|
|
7612
|
+
const storage = grantStorage(stateDir, () => ctx.get("credentials"), origin);
|
|
7242
7613
|
const login = new PluginLogin({
|
|
7243
7614
|
origin,
|
|
7244
7615
|
userAgent,
|
|
@@ -7249,7 +7620,7 @@ function apply(ctx, config) {
|
|
|
7249
7620
|
});
|
|
7250
7621
|
ctx.inject(["credentials"], (scoped) => {
|
|
7251
7622
|
scoped.on("credentials/record-updated", (key) => {
|
|
7252
|
-
if (key ===
|
|
7623
|
+
if (key === gatewayGrantKey(origin)) login.restore();
|
|
7253
7624
|
});
|
|
7254
7625
|
scoped.effect(async () => {
|
|
7255
7626
|
await login.restore();
|
|
@@ -7260,7 +7631,7 @@ function apply(ctx, config) {
|
|
|
7260
7631
|
origin,
|
|
7261
7632
|
plugin: login,
|
|
7262
7633
|
account: () => ctx.get("deepseekAccount"),
|
|
7263
|
-
apiKey: () =>
|
|
7634
|
+
apiKey: () => developmentKey(origin, config.apiKeyEnv)
|
|
7264
7635
|
});
|
|
7265
7636
|
const http = new GatewayHttp({
|
|
7266
7637
|
origin,
|
|
@@ -7268,7 +7639,7 @@ function apply(ctx, config) {
|
|
|
7268
7639
|
resolveCredential: () => chain.resolve(),
|
|
7269
7640
|
rejectToken: (credential) => chain.reject(credential)
|
|
7270
7641
|
});
|
|
7271
|
-
const store = new TaskStore(stateDir);
|
|
7642
|
+
const store = new TaskStore(gatewayStateDirectory(stateDir, origin));
|
|
7272
7643
|
const tracker = new TaskTracker({
|
|
7273
7644
|
http,
|
|
7274
7645
|
store,
|