@vibedev-si/dsh-vibedev 0.3.1 → 0.3.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 +3 -0
- package/THIRD-PARTY-NOTICES.txt +1 -1
- package/client/client.js +53 -5
- package/lib/account/index.d.ts +50 -1
- package/lib/gateway/http.d.ts +5 -0
- package/lib/index.js +196 -22
- package/lib/llm/adapter.d.ts +2 -0
- package/lib/llm/index.d.ts +2 -0
- package/lib/tasks/tracker.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
- 图片按张计费,视频按秒计费(参考视频的时长也计入),音乐和播客按次计费,语音按原始秒计费(按服务商返回的原始时长,可能和播放时长不同),转写按分钟计费。`media_models` 会列出当前价格;目录没有给出价格的模型显示「未定价」,不代表免费。
|
|
98
98
|
- 视频和音频只在成功时扣费,失败和取消的任务不扣。视频提交后无法取消。
|
|
99
99
|
- 同一次操作只会生成一次:提交没有得到确认时,会用同一个请求键向网关确认结果;查询或下载失败只会重新查询、刷新链接,不会重新提交。结果已经生成、但费用还在结算(`settlement_pending`)时,结果照常保存,不会重新生成。
|
|
100
|
+
- 余额在每次付费请求结束后(对话回复、生成、转写)马上重新读取,侧栏和设置页几秒内就能看到扣费后的余额。
|
|
100
101
|
- 插件设置里的「每次付费生成前先询问」默认关闭。打开后,每次付费的媒体请求之前都会先弹出确认,并显示预估金额。
|
|
101
102
|
|
|
102
103
|
## 设置
|
|
@@ -162,6 +163,8 @@ Everything is paid from your VibeDev balance; the DeepSeek account stays DeepSee
|
|
|
162
163
|
|
|
163
164
|
**Gateway audio (0.3.1)**: audio models are recognized from the catalog's structured `audio` block (music, podcast, speech, transcription), never from their names; speech generation (Seed) joins music and podcasts; prices show their unit (per generation, per second of original audio, per minute) and "not priced" when the catalog states none; the `vibedevMedia` service gains `startAudio` with an operation key and query-only result reading.
|
|
164
165
|
|
|
166
|
+
**Balance without the wait (0.3.2)**: the balance is read again right after a paid request ends (a chat reply, a generation, a transcription) and the account pages hear it through a long poll, so a charge shows within seconds instead of after up to two minutes. A page that already has a balance is answered at once while a newer one is read behind it; reads after charges are at least five seconds apart.
|
|
167
|
+
|
|
165
168
|
**Models**: the list comes from the VibeDev gateway for your account, so nothing is listed before you sign in; models appear right after, without a restart. Using a VibeDev model while signed out, or after the sign-in has ended, asks you to sign in; a used-up balance says so with the top-up link. These messages are separate from the DeepSeek account's. An expired access token is renewed and the request sent again, without repeating any output.
|
|
166
169
|
|
|
167
170
|
**Web search**: the plugin registers the web search provider `vibedev-gateway`, signed in with your VibeDev account. Registering does not select it: DeepSeek Harness keeps its own search unless the profile sets `searchProvider: vibedev-gateway` on the `web` row (the VibeDev app does). Searching is free, but the gateway refuses every request, searches included, while the balance is zero.
|
package/THIRD-PARTY-NOTICES.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
THIRD-PARTY NOTICES · @vibedev-si/dsh-vibedev 0.3.
|
|
1
|
+
THIRD-PARTY NOTICES · @vibedev-si/dsh-vibedev 0.3.2
|
|
2
2
|
|
|
3
3
|
This package bundles code from the projects below. Each is used under the licence that
|
|
4
4
|
follows its name; the licence texts are reproduced as the projects ship them.
|
package/client/client.js
CHANGED
|
@@ -9,6 +9,8 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
|
|
|
9
9
|
const ACCOUNT_ROUTE = "/api/dsh-vibedev/account";
|
|
10
10
|
const PENDING_POLL_MS = 2e3;
|
|
11
11
|
const IDLE_POLL_MS = 6e4;
|
|
12
|
+
/** After a failed long poll, the next one waits this long (the Host may be restarting). */
|
|
13
|
+
const WATCH_RETRY_MS = 5e3;
|
|
12
14
|
/** Who to show: the nickname, else the email. */
|
|
13
15
|
function displayName(view) {
|
|
14
16
|
return view?.user?.nickname ?? view?.user?.email;
|
|
@@ -33,6 +35,8 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
|
|
|
33
35
|
reading;
|
|
34
36
|
stopped = true;
|
|
35
37
|
unsubscribeFocus;
|
|
38
|
+
/** The open long poll, when one is. */
|
|
39
|
+
watching;
|
|
36
40
|
/**
|
|
37
41
|
* @param options - fetch, timers, window opening and focus (test seams).
|
|
38
42
|
*/
|
|
@@ -82,17 +86,61 @@ window.__ModuleLoader__.load({ id: "@vibedev-si/dsh-vibedev", factory: (require)
|
|
|
82
86
|
globalThis.clearTimeout(handle);
|
|
83
87
|
}))(this.timer);
|
|
84
88
|
this.timer = void 0;
|
|
89
|
+
this.watching?.abort();
|
|
90
|
+
this.watching = void 0;
|
|
85
91
|
}
|
|
86
|
-
|
|
87
|
-
if (this.stopped) return;
|
|
92
|
+
later(callback, ms) {
|
|
88
93
|
if (this.timer !== void 0) (this.options.clearTimeout ?? ((handle) => {
|
|
89
94
|
globalThis.clearTimeout(handle);
|
|
90
95
|
}))(this.timer);
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
this.timer = (this.options.setTimeout ?? ((run, delay) => globalThis.setTimeout(run, delay)))(() => {
|
|
97
|
+
this.timer = void 0;
|
|
98
|
+
callback();
|
|
99
|
+
}, ms);
|
|
100
|
+
}
|
|
101
|
+
schedule() {
|
|
102
|
+
if (this.stopped) return;
|
|
103
|
+
const view = this.state.view;
|
|
104
|
+
if (view?.pending === void 0 && typeof view?.revision === "number") {
|
|
105
|
+
if (this.timer !== void 0) (this.options.clearTimeout ?? ((handle) => {
|
|
106
|
+
globalThis.clearTimeout(handle);
|
|
107
|
+
}))(this.timer);
|
|
93
108
|
this.timer = void 0;
|
|
109
|
+
this.watch(view.revision);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.watching?.abort();
|
|
113
|
+
this.watching = void 0;
|
|
114
|
+
this.later(() => {
|
|
94
115
|
this.refresh();
|
|
95
|
-
},
|
|
116
|
+
}, view?.pending !== void 0 ? PENDING_POLL_MS : IDLE_POLL_MS);
|
|
117
|
+
}
|
|
118
|
+
/** Keep one long poll open for changes past `revision`. */
|
|
119
|
+
watch(revision) {
|
|
120
|
+
if (this.watching !== void 0) return;
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
this.watching = controller;
|
|
123
|
+
(async () => {
|
|
124
|
+
let failed = false;
|
|
125
|
+
try {
|
|
126
|
+
const response = await this.fetch(`${ACCOUNT_ROUTE}?since=${revision}`, { signal: controller.signal });
|
|
127
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
128
|
+
const view = await response.json();
|
|
129
|
+
if (!controller.signal.aborted && (view.revision ?? 0) >= (this.state.view?.revision ?? 0)) this.set({
|
|
130
|
+
view,
|
|
131
|
+
loadFailed: false
|
|
132
|
+
});
|
|
133
|
+
} catch {
|
|
134
|
+
failed = !controller.signal.aborted;
|
|
135
|
+
} finally {
|
|
136
|
+
if (this.watching === controller) this.watching = void 0;
|
|
137
|
+
}
|
|
138
|
+
if (this.stopped || controller.signal.aborted) return;
|
|
139
|
+
if (failed) this.later(() => {
|
|
140
|
+
this.refresh();
|
|
141
|
+
}, WATCH_RETRY_MS);
|
|
142
|
+
else this.schedule();
|
|
143
|
+
})();
|
|
96
144
|
}
|
|
97
145
|
/**
|
|
98
146
|
* Read the account now.
|
package/lib/account/index.d.ts
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* app's account, the plugin's own sign-in, a development key, or none), who
|
|
8
8
|
* is signed in, the balance, a sign-in waiting for the browser, and how many
|
|
9
9
|
* VibeDev models the pickers list; `?fresh=1` reads the balance again;
|
|
10
|
+
* `?since=<revision>` holds the request until the person or the balance
|
|
11
|
+
* changes (a charge is read again right after it ends), so the pages show a
|
|
12
|
+
* new balance within seconds instead of on their next minute poll;
|
|
10
13
|
* - `POST /api/dsh-vibedev/account/sign-in` — `{ open?: boolean }`: start the
|
|
11
14
|
* plugin's own sign-in; answers the authorization link (`open: false` leaves
|
|
12
15
|
* opening it to the page);
|
|
@@ -58,6 +61,11 @@ export interface AccountView {
|
|
|
58
61
|
};
|
|
59
62
|
/** The VibeDev account is the app's main account (the VibeDev app): its row leads the sidebar foot and Settings. */
|
|
60
63
|
readonly primary: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Grows whenever the person or the balance changes; `GET …/account?since=<revision>`
|
|
66
|
+
* answers as soon as it grows (or after a while with the same view).
|
|
67
|
+
*/
|
|
68
|
+
readonly revision: number;
|
|
61
69
|
}
|
|
62
70
|
/** What the account routes need from the plugin. */
|
|
63
71
|
export interface AccountServiceOptions {
|
|
@@ -74,16 +82,51 @@ export interface AccountServiceOptions {
|
|
|
74
82
|
readonly primary?: boolean;
|
|
75
83
|
readonly fetch?: typeof globalThis.fetch;
|
|
76
84
|
readonly now?: () => number;
|
|
85
|
+
/** Timer seams for the reads after a charge and the long poll. */
|
|
86
|
+
readonly setTimeout?: (callback: () => void, ms: number) => unknown;
|
|
87
|
+
readonly clearTimeout?: (handle: unknown) => void;
|
|
77
88
|
}
|
|
78
89
|
/** The account state behind the pages. */
|
|
79
90
|
export declare class AccountService {
|
|
80
91
|
private readonly options;
|
|
81
92
|
private balanceCache;
|
|
93
|
+
/** A background read of a stale balance, so only one runs at a time. */
|
|
94
|
+
private revalidating;
|
|
82
95
|
private readonly now;
|
|
96
|
+
private revision;
|
|
97
|
+
/** What the last read showed, to tell whether the next one changed anything. */
|
|
98
|
+
private lastSeen;
|
|
99
|
+
private readonly waiters;
|
|
100
|
+
private settleTimers;
|
|
101
|
+
/** Grows with every credential change; a read started before one is dropped. */
|
|
102
|
+
private epoch;
|
|
103
|
+
/** When the last read after a charge started, to space them out. */
|
|
104
|
+
private lastSpendReadAt;
|
|
83
105
|
/**
|
|
84
106
|
* @param options - origin, credential chain, the plugin's sign-in and the route's model state.
|
|
85
107
|
*/
|
|
86
108
|
constructor(options: AccountServiceOptions);
|
|
109
|
+
private timeout;
|
|
110
|
+
private cancelTimeout;
|
|
111
|
+
/** Something the pages show changed: answer every waiting `?since=` request. */
|
|
112
|
+
changed(): void;
|
|
113
|
+
/** The account changed hands (sign-in, sign-out, switch): forget the balance and tell the pages. */
|
|
114
|
+
credentialChanged(): void;
|
|
115
|
+
/**
|
|
116
|
+
* A request that may have cost money ended (a chat reply, a generation, a
|
|
117
|
+
* transcription): read the balance again shortly, and once more later if the
|
|
118
|
+
* charge had not settled yet. Repeated calls in a burst share the reads.
|
|
119
|
+
*/
|
|
120
|
+
spent(): void;
|
|
121
|
+
/** Read the balance of the current credential now (the pages hear about a change through the long poll). */
|
|
122
|
+
private readNow;
|
|
123
|
+
/**
|
|
124
|
+
* Wait until the view changes past `since`, or a while.
|
|
125
|
+
* @param since - the revision the page shows.
|
|
126
|
+
* @param signal - the page went away.
|
|
127
|
+
* @returns once there is something new, or the wait ran out.
|
|
128
|
+
*/
|
|
129
|
+
wait(since: number, signal?: AbortSignal): Promise<void>;
|
|
87
130
|
/** Links to the gateway's pages. */
|
|
88
131
|
links(): AccountView['links'];
|
|
89
132
|
/**
|
|
@@ -92,8 +135,12 @@ export declare class AccountService {
|
|
|
92
135
|
* @returns the view.
|
|
93
136
|
*/
|
|
94
137
|
view(fresh?: boolean): Promise<AccountView>;
|
|
95
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* `GET /v1/account/auth/me` for one token. A read younger than the TTL is
|
|
140
|
+
* reused; an older one is answered at once while a new read runs behind it.
|
|
141
|
+
*/
|
|
96
142
|
private me;
|
|
143
|
+
private read;
|
|
97
144
|
/**
|
|
98
145
|
* Start the plugin's own sign-in.
|
|
99
146
|
* @param open - open the system browser (false: the page opens the link itself).
|
|
@@ -111,6 +158,8 @@ export declare class AccountService {
|
|
|
111
158
|
* @returns false when the credential is the app's account, which is signed out in the app.
|
|
112
159
|
*/
|
|
113
160
|
signOut(): Promise<boolean>;
|
|
161
|
+
/** Stop the pending reads after a charge (the plugin is going away). */
|
|
162
|
+
dispose(): void;
|
|
114
163
|
}
|
|
115
164
|
/**
|
|
116
165
|
* The account routes.
|
package/lib/gateway/http.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export interface GatewayHttpOptions {
|
|
|
31
31
|
readonly userAgent: string;
|
|
32
32
|
readonly fetch?: typeof globalThis.fetch;
|
|
33
33
|
readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
34
|
+
/** Told about every request the gateway accepted (2xx), after the fact. */
|
|
35
|
+
readonly onAccepted?: (request: {
|
|
36
|
+
readonly method: string;
|
|
37
|
+
readonly path: string;
|
|
38
|
+
}) => void;
|
|
34
39
|
}
|
|
35
40
|
/** One request. */
|
|
36
41
|
export interface GatewayRequest {
|
package/lib/index.js
CHANGED
|
@@ -19,15 +19,41 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
|
19
19
|
//#region src/account/index.ts
|
|
20
20
|
/** Path prefix of the plugin's Host routes. */
|
|
21
21
|
const ACCOUNT_ROUTE_PREFIX = "/api/dsh-vibedev/account";
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* How long a balance read is reused as it is. After that the cached value is
|
|
24
|
+
* still answered at once while a new read runs in the background (the page
|
|
25
|
+
* hears the result through the long poll), so a page never waits on the gateway
|
|
26
|
+
* when it already has a balance.
|
|
27
|
+
*/
|
|
28
|
+
const BALANCE_TTL_MS = 3e4;
|
|
29
|
+
/** How long `?since=` holds a request open when nothing changes. */
|
|
30
|
+
const WAIT_MS = 25e3;
|
|
31
|
+
/**
|
|
32
|
+
* When the balance is read again after a charge: the gateway settles a charge
|
|
33
|
+
* shortly after the request ends, so it is read a moment later, and once more
|
|
34
|
+
* if the first read still shows the old amount.
|
|
35
|
+
*/
|
|
36
|
+
const SETTLE_READS_MS = [1500, 8e3];
|
|
37
|
+
/** Reads after charges are at least this far apart. */
|
|
38
|
+
const MIN_READ_SPACING_MS = 5e3;
|
|
24
39
|
const record$8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
25
40
|
const text$8 = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
26
41
|
/** The account state behind the pages. */
|
|
27
42
|
var AccountService = class {
|
|
28
43
|
options;
|
|
29
44
|
balanceCache;
|
|
45
|
+
/** A background read of a stale balance, so only one runs at a time. */
|
|
46
|
+
revalidating;
|
|
30
47
|
now;
|
|
48
|
+
revision = 0;
|
|
49
|
+
/** What the last read showed, to tell whether the next one changed anything. */
|
|
50
|
+
lastSeen;
|
|
51
|
+
waiters = /* @__PURE__ */ new Set();
|
|
52
|
+
settleTimers = [];
|
|
53
|
+
/** Grows with every credential change; a read started before one is dropped. */
|
|
54
|
+
epoch = 0;
|
|
55
|
+
/** When the last read after a charge started, to space them out. */
|
|
56
|
+
lastSpendReadAt = Number.NEGATIVE_INFINITY;
|
|
31
57
|
/**
|
|
32
58
|
* @param options - origin, credential chain, the plugin's sign-in and the route's model state.
|
|
33
59
|
*/
|
|
@@ -35,6 +61,73 @@ var AccountService = class {
|
|
|
35
61
|
this.options = options;
|
|
36
62
|
this.now = options.now ?? Date.now;
|
|
37
63
|
}
|
|
64
|
+
timeout(callback, ms) {
|
|
65
|
+
return (this.options.setTimeout ?? ((run, delay) => {
|
|
66
|
+
const handle = globalThis.setTimeout(run, delay);
|
|
67
|
+
handle.unref?.();
|
|
68
|
+
return handle;
|
|
69
|
+
}))(callback, ms);
|
|
70
|
+
}
|
|
71
|
+
cancelTimeout(handle) {
|
|
72
|
+
(this.options.clearTimeout ?? ((value) => {
|
|
73
|
+
globalThis.clearTimeout(value);
|
|
74
|
+
}))(handle);
|
|
75
|
+
}
|
|
76
|
+
/** Something the pages show changed: answer every waiting `?since=` request. */
|
|
77
|
+
changed() {
|
|
78
|
+
this.revision++;
|
|
79
|
+
const waiting = [...this.waiters];
|
|
80
|
+
this.waiters.clear();
|
|
81
|
+
for (const wake of waiting) wake();
|
|
82
|
+
}
|
|
83
|
+
/** The account changed hands (sign-in, sign-out, switch): forget the balance and tell the pages. */
|
|
84
|
+
credentialChanged() {
|
|
85
|
+
this.epoch++;
|
|
86
|
+
this.balanceCache = void 0;
|
|
87
|
+
this.revalidating = void 0;
|
|
88
|
+
this.lastSeen = void 0;
|
|
89
|
+
this.changed();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A request that may have cost money ended (a chat reply, a generation, a
|
|
93
|
+
* transcription): read the balance again shortly, and once more later if the
|
|
94
|
+
* charge had not settled yet. Repeated calls in a burst share the reads.
|
|
95
|
+
*/
|
|
96
|
+
spent() {
|
|
97
|
+
for (const handle of this.settleTimers) this.cancelTimeout(handle);
|
|
98
|
+
const before = this.lastSeen;
|
|
99
|
+
const spacing = Math.max(0, this.lastSpendReadAt + MIN_READ_SPACING_MS - this.now());
|
|
100
|
+
this.settleTimers = SETTLE_READS_MS.map((delay, index) => this.timeout(() => {
|
|
101
|
+
if (index > 0 && this.lastSeen !== before) return;
|
|
102
|
+
this.readNow();
|
|
103
|
+
}, Math.max(delay, spacing + (index === 0 ? 0 : delay - SETTLE_READS_MS[0]))));
|
|
104
|
+
}
|
|
105
|
+
/** Read the balance of the current credential now (the pages hear about a change through the long poll). */
|
|
106
|
+
async readNow() {
|
|
107
|
+
this.lastSpendReadAt = this.now();
|
|
108
|
+
const credential = await this.options.chain.resolve().catch(() => void 0);
|
|
109
|
+
if (credential !== void 0) await this.me(credential.token, true);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Wait until the view changes past `since`, or a while.
|
|
113
|
+
* @param since - the revision the page shows.
|
|
114
|
+
* @param signal - the page went away.
|
|
115
|
+
* @returns once there is something new, or the wait ran out.
|
|
116
|
+
*/
|
|
117
|
+
async wait(since, signal) {
|
|
118
|
+
if (since !== this.revision || signal?.aborted === true) return;
|
|
119
|
+
await new Promise((resolve) => {
|
|
120
|
+
const done = () => {
|
|
121
|
+
this.waiters.delete(done);
|
|
122
|
+
this.cancelTimeout(timer);
|
|
123
|
+
signal?.removeEventListener("abort", done);
|
|
124
|
+
resolve();
|
|
125
|
+
};
|
|
126
|
+
const timer = this.timeout(done, WAIT_MS);
|
|
127
|
+
this.waiters.add(done);
|
|
128
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
129
|
+
});
|
|
130
|
+
}
|
|
38
131
|
/** Links to the gateway's pages. */
|
|
39
132
|
links() {
|
|
40
133
|
const origin = this.options.origin;
|
|
@@ -62,20 +155,39 @@ var AccountService = class {
|
|
|
62
155
|
...pending === void 0 ? {} : { pending },
|
|
63
156
|
...attempt === void 0 ? {} : { attempt }
|
|
64
157
|
};
|
|
65
|
-
if (credential === void 0) return
|
|
66
|
-
|
|
67
|
-
|
|
158
|
+
if (credential === void 0) return {
|
|
159
|
+
...base,
|
|
160
|
+
revision: this.revision
|
|
161
|
+
};
|
|
162
|
+
const [grantUser, me] = await Promise.all([credential.kind === "plugin" ? this.options.login.user().catch(() => void 0) : Promise.resolve(void 0), this.me(credential.token, fresh)]);
|
|
68
163
|
const user = me.user ?? userOf(grantUser);
|
|
69
164
|
return {
|
|
70
165
|
...base,
|
|
71
166
|
...user === void 0 ? {} : { user },
|
|
72
|
-
...me.balance === void 0 ? {} : { balance: me.balance }
|
|
167
|
+
...me.balance === void 0 ? {} : { balance: me.balance },
|
|
168
|
+
revision: this.revision
|
|
73
169
|
};
|
|
74
170
|
}
|
|
75
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* `GET /v1/account/auth/me` for one token. A read younger than the TTL is
|
|
173
|
+
* reused; an older one is answered at once while a new read runs behind it.
|
|
174
|
+
*/
|
|
76
175
|
me(token, fresh) {
|
|
77
176
|
const cached = this.balanceCache;
|
|
78
|
-
if (!fresh && cached !== void 0 && cached.token === token
|
|
177
|
+
if (!fresh && cached !== void 0 && cached.token === token) {
|
|
178
|
+
if (this.now() - cached.at >= BALANCE_TTL_MS && this.revalidating === void 0) {
|
|
179
|
+
const next = this.read(token);
|
|
180
|
+
this.revalidating = next;
|
|
181
|
+
next.finally(() => {
|
|
182
|
+
if (this.revalidating === next) this.revalidating = void 0;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return cached.value;
|
|
186
|
+
}
|
|
187
|
+
return this.read(token);
|
|
188
|
+
}
|
|
189
|
+
read(token) {
|
|
190
|
+
const epoch = this.epoch;
|
|
79
191
|
const value = (async () => {
|
|
80
192
|
try {
|
|
81
193
|
const response = await (this.options.fetch ?? globalThis.fetch)(`${this.options.origin}/v1/account/auth/me`, {
|
|
@@ -111,13 +223,28 @@ var AccountService = class {
|
|
|
111
223
|
return {};
|
|
112
224
|
}
|
|
113
225
|
})();
|
|
114
|
-
this.balanceCache
|
|
226
|
+
const previous = this.balanceCache;
|
|
227
|
+
if (previous === void 0 || previous.token !== token) this.balanceCache = {
|
|
115
228
|
token,
|
|
116
229
|
at: this.now(),
|
|
117
230
|
value
|
|
118
231
|
};
|
|
119
232
|
value.then((read) => {
|
|
120
|
-
if (
|
|
233
|
+
if (epoch !== this.epoch) return;
|
|
234
|
+
if (read.balance === void 0) {
|
|
235
|
+
if (this.balanceCache?.value === value) this.balanceCache = void 0;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.balanceCache = {
|
|
239
|
+
token,
|
|
240
|
+
at: this.now(),
|
|
241
|
+
value
|
|
242
|
+
};
|
|
243
|
+
const seen = JSON.stringify(read);
|
|
244
|
+
if (this.lastSeen !== void 0 && this.lastSeen !== seen) {
|
|
245
|
+
this.lastSeen = seen;
|
|
246
|
+
this.changed();
|
|
247
|
+
} else this.lastSeen = seen;
|
|
121
248
|
});
|
|
122
249
|
return value;
|
|
123
250
|
}
|
|
@@ -145,9 +272,17 @@ var AccountService = class {
|
|
|
145
272
|
async signOut() {
|
|
146
273
|
if ((await this.options.chain.resolve().catch(() => void 0))?.kind === "account") return false;
|
|
147
274
|
await this.options.login.signOut();
|
|
148
|
-
this.
|
|
275
|
+
this.credentialChanged();
|
|
149
276
|
return true;
|
|
150
277
|
}
|
|
278
|
+
/** Stop the pending reads after a charge (the plugin is going away). */
|
|
279
|
+
dispose() {
|
|
280
|
+
for (const handle of this.settleTimers) this.cancelTimeout(handle);
|
|
281
|
+
this.settleTimers = [];
|
|
282
|
+
const waiting = [...this.waiters];
|
|
283
|
+
this.waiters.clear();
|
|
284
|
+
for (const wake of waiting) wake();
|
|
285
|
+
}
|
|
151
286
|
};
|
|
152
287
|
function userOf(user) {
|
|
153
288
|
if (user === void 0) return void 0;
|
|
@@ -189,7 +324,10 @@ function accountRoutes(account) {
|
|
|
189
324
|
requestBody: "buffered",
|
|
190
325
|
fetch: async (request) => {
|
|
191
326
|
try {
|
|
192
|
-
|
|
327
|
+
const query = new URL(request.url, "http://host").searchParams;
|
|
328
|
+
const since = query.get("since");
|
|
329
|
+
if (since !== null && /^\d{1,15}$/.test(since)) await account.wait(Number(since), request.signal);
|
|
330
|
+
return json(200, await account.view(query.get("fresh") === "1"));
|
|
193
331
|
} catch (error) {
|
|
194
332
|
return failure(error);
|
|
195
333
|
}
|
|
@@ -2761,6 +2899,7 @@ var GatewayAdapter = class extends LlmAdapter {
|
|
|
2761
2899
|
const upstream = options.signal === void 0 ? consumer.signal : AbortSignal.any([options.signal, consumer.signal]);
|
|
2762
2900
|
const streamIdleTimeoutMs = this.config.streamIdleTimeoutMs ?? 3e5;
|
|
2763
2901
|
const watchdog = _usingCtx$1.u(idleWatchdog(upstream, streamIdleTimeoutMs, "LLM_STREAM_IDLE_TIMEOUT"));
|
|
2902
|
+
let answered = false;
|
|
2764
2903
|
try {
|
|
2765
2904
|
const containsImage = options.messages.some((message) => contentHasImage(message.content));
|
|
2766
2905
|
if (containsImage && !model.input.includes("image")) throw new LlmError(`VibeDev model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
|
|
@@ -2797,6 +2936,7 @@ var GatewayAdapter = class extends LlmAdapter {
|
|
|
2797
2936
|
...init,
|
|
2798
2937
|
redirect: "error"
|
|
2799
2938
|
});
|
|
2939
|
+
answered = true;
|
|
2800
2940
|
response = {
|
|
2801
2941
|
status: received.status,
|
|
2802
2942
|
headers: Object.fromEntries(received.headers.entries())
|
|
@@ -2882,6 +3022,9 @@ var GatewayAdapter = class extends LlmAdapter {
|
|
|
2882
3022
|
throw error;
|
|
2883
3023
|
} finally {
|
|
2884
3024
|
consumer.abort("gateway stream consumer stopped");
|
|
3025
|
+
if (answered) try {
|
|
3026
|
+
this.config.onRequestEnded?.();
|
|
3027
|
+
} catch {}
|
|
2885
3028
|
}
|
|
2886
3029
|
} catch (_) {
|
|
2887
3030
|
_usingCtx$1.e = _;
|
|
@@ -3034,7 +3177,8 @@ function installGatewayModels(ctx, options) {
|
|
|
3034
3177
|
resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath), ref),
|
|
3035
3178
|
onReplayDegrade: ({ provider, model, reason }) => {
|
|
3036
3179
|
ctx.logger.warn(`dsh-vibedev: unusable replay state on assistant history for "${provider}/${model}"; sending provider-neutral content (${reason})`);
|
|
3037
|
-
}
|
|
3180
|
+
},
|
|
3181
|
+
...options.onRequestEnded === void 0 ? {} : { onRequestEnded: options.onRequestEnded }
|
|
3038
3182
|
});
|
|
3039
3183
|
const registration = ctx.llm.registerAdapter([VIBEDEV_ROUTE], adapter);
|
|
3040
3184
|
ctx.inject(["settings"], (child) => {
|
|
@@ -3380,12 +3524,21 @@ var GatewayHttp = class {
|
|
|
3380
3524
|
*/
|
|
3381
3525
|
async send(path, request = {}) {
|
|
3382
3526
|
const url = this.url(path);
|
|
3383
|
-
|
|
3527
|
+
const authenticate = request.anonymous !== true && this.isGatewayUrl(url);
|
|
3528
|
+
let credential = authenticate ? await this.credential() : void 0;
|
|
3384
3529
|
let refreshed = false;
|
|
3385
3530
|
let busyRetries = request.busyRetries ?? 2;
|
|
3386
3531
|
for (;;) {
|
|
3387
3532
|
const response = await this.attempt(url, request, credential);
|
|
3388
|
-
if (response.ok)
|
|
3533
|
+
if (response.ok) {
|
|
3534
|
+
if (authenticate) try {
|
|
3535
|
+
this.options.onAccepted?.({
|
|
3536
|
+
method: request.method ?? "GET",
|
|
3537
|
+
path: new URL(url).pathname
|
|
3538
|
+
});
|
|
3539
|
+
} catch {}
|
|
3540
|
+
return response;
|
|
3541
|
+
}
|
|
3389
3542
|
const failure = parseGatewayFailure(await response.clone().json().catch(() => void 0));
|
|
3390
3543
|
if (response.status === 401 && credential !== void 0 && credential.kind !== "key" && !refreshed) {
|
|
3391
3544
|
refreshed = true;
|
|
@@ -7353,11 +7506,15 @@ var TaskTracker = class {
|
|
|
7353
7506
|
}
|
|
7354
7507
|
async finish(id, patch) {
|
|
7355
7508
|
this.downloadFailures.delete(id);
|
|
7356
|
-
|
|
7509
|
+
const record = await this.update(id, {
|
|
7357
7510
|
...patch,
|
|
7358
7511
|
progress: void 0,
|
|
7359
7512
|
finishedAt: this.now()
|
|
7360
7513
|
});
|
|
7514
|
+
try {
|
|
7515
|
+
this.options.onFinished?.(record);
|
|
7516
|
+
} catch {}
|
|
7517
|
+
return record;
|
|
7361
7518
|
}
|
|
7362
7519
|
/** Hold (or keep) this process's claim on a task, renewing it while held. */
|
|
7363
7520
|
async holdClaim(id) {
|
|
@@ -8415,6 +8572,8 @@ function audioTranscribeTool(runtime) {
|
|
|
8415
8572
|
* ```
|
|
8416
8573
|
* @module dsh-vibedev
|
|
8417
8574
|
*/
|
|
8575
|
+
/** Requests that are charged: generations and transcriptions (uploads and reads are free). */
|
|
8576
|
+
const PAID_PATH = /^\/v1\/(?:images|videos|audio)(?:\/|$)/;
|
|
8418
8577
|
/** The package version, sent in the `User-Agent` the gateway attributes plugin traffic by. */
|
|
8419
8578
|
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
8420
8579
|
const name = "dsh-vibedev";
|
|
@@ -8509,17 +8668,25 @@ function apply(ctx, config) {
|
|
|
8509
8668
|
account: () => ctx.get("deepseekAccount"),
|
|
8510
8669
|
apiKey: () => developmentKey(origin, config.apiKeyEnv)
|
|
8511
8670
|
});
|
|
8671
|
+
let account;
|
|
8672
|
+
const spent = () => {
|
|
8673
|
+
account?.spent();
|
|
8674
|
+
};
|
|
8512
8675
|
const http = new GatewayHttp({
|
|
8513
8676
|
origin,
|
|
8514
8677
|
userAgent,
|
|
8515
8678
|
resolveCredential: () => chain.resolve(),
|
|
8516
|
-
rejectToken: (credential) => chain.reject(credential)
|
|
8679
|
+
rejectToken: (credential) => chain.reject(credential),
|
|
8680
|
+
onAccepted: ({ method, path }) => {
|
|
8681
|
+
if (method === "POST" && PAID_PATH.test(path)) spent();
|
|
8682
|
+
}
|
|
8517
8683
|
});
|
|
8518
8684
|
const store = new TaskStore(gatewayStateDirectory(stateDir, origin));
|
|
8519
8685
|
const tracker = new TaskTracker({
|
|
8520
8686
|
http,
|
|
8521
8687
|
store,
|
|
8522
|
-
log: warn
|
|
8688
|
+
log: warn,
|
|
8689
|
+
onFinished: spent
|
|
8523
8690
|
});
|
|
8524
8691
|
const slots = {
|
|
8525
8692
|
image: config.imageModel,
|
|
@@ -8573,7 +8740,8 @@ function apply(ctx, config) {
|
|
|
8573
8740
|
deviceId: () => storage.deviceId(),
|
|
8574
8741
|
preferredModels: () => config.preferredModels,
|
|
8575
8742
|
onCredentialChange: (listener) => login.onChange(listener),
|
|
8576
|
-
catalogRefreshMinutes: config.catalogRefreshMinutes
|
|
8743
|
+
catalogRefreshMinutes: config.catalogRefreshMinutes,
|
|
8744
|
+
onRequestEnded: spent
|
|
8577
8745
|
});
|
|
8578
8746
|
llmCtx.effect(() => () => {
|
|
8579
8747
|
route = void 0;
|
|
@@ -8590,7 +8758,7 @@ function apply(ctx, config) {
|
|
|
8590
8758
|
maxRetryWaitMs: 1e4
|
|
8591
8759
|
}));
|
|
8592
8760
|
});
|
|
8593
|
-
const
|
|
8761
|
+
const accountService = new AccountService({
|
|
8594
8762
|
origin,
|
|
8595
8763
|
userAgent,
|
|
8596
8764
|
chain,
|
|
@@ -8601,8 +8769,9 @@ function apply(ctx, config) {
|
|
|
8601
8769
|
...route?.hidden() === void 0 ? {} : { hidden: route.hidden() }
|
|
8602
8770
|
})
|
|
8603
8771
|
});
|
|
8772
|
+
account = accountService;
|
|
8604
8773
|
ctx.inject(["connection"], (scoped) => {
|
|
8605
|
-
for (const accountRoute of accountRoutes(
|
|
8774
|
+
for (const accountRoute of accountRoutes(accountService)) scoped.effect(() => scoped.connection.fetch.register(accountRoute), `dsh-vibedev: ${accountRoute.path}`);
|
|
8606
8775
|
});
|
|
8607
8776
|
ctx.effect(() => {
|
|
8608
8777
|
tracker.resume().catch((error) => {
|
|
@@ -8610,9 +8779,11 @@ function apply(ctx, config) {
|
|
|
8610
8779
|
});
|
|
8611
8780
|
const unsubscribe = login.onChange(() => {
|
|
8612
8781
|
runtime.invalidateCatalog();
|
|
8782
|
+
accountService.credentialChanged();
|
|
8613
8783
|
});
|
|
8614
8784
|
return () => {
|
|
8615
8785
|
unsubscribe();
|
|
8786
|
+
accountService.dispose();
|
|
8616
8787
|
tracker.dispose();
|
|
8617
8788
|
login.dispose();
|
|
8618
8789
|
};
|
|
@@ -8622,7 +8793,10 @@ function apply(ctx, config) {
|
|
|
8622
8793
|
const controller = new AbortController();
|
|
8623
8794
|
(async () => {
|
|
8624
8795
|
try {
|
|
8625
|
-
for await (const _view of accountCtx.deepseekAccount.watch(controller.signal))
|
|
8796
|
+
for await (const _view of accountCtx.deepseekAccount.watch(controller.signal)) {
|
|
8797
|
+
runtime.invalidateCatalog();
|
|
8798
|
+
accountService.credentialChanged();
|
|
8799
|
+
}
|
|
8626
8800
|
} catch {}
|
|
8627
8801
|
})();
|
|
8628
8802
|
return () => {
|
package/lib/llm/adapter.d.ts
CHANGED
|
@@ -87,6 +87,8 @@ export interface GatewayAdapterOptions {
|
|
|
87
87
|
model: string;
|
|
88
88
|
reason: string;
|
|
89
89
|
}) => void;
|
|
90
|
+
/** A request reached the gateway and ended, however it ended: it may have been charged. */
|
|
91
|
+
readonly onRequestEnded?: () => void;
|
|
90
92
|
}
|
|
91
93
|
/**
|
|
92
94
|
* The gateway as an LLM adapter. Each operation reads the current catalog, so a refresh reaches
|
package/lib/llm/index.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface GatewayModelsOptions {
|
|
|
43
43
|
readonly catalogRefreshMinutes?: number;
|
|
44
44
|
readonly catalogTimeoutMs?: number;
|
|
45
45
|
readonly cacheRetention?: 'none' | 'short' | 'long';
|
|
46
|
+
/** A model request that may have been charged ended (the account reads the balance again). */
|
|
47
|
+
readonly onRequestEnded?: () => void;
|
|
46
48
|
}
|
|
47
49
|
/** The route's live state, for status lines and tests. */
|
|
48
50
|
export interface GatewayModels {
|
package/lib/tasks/tracker.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface TrackerOptions {
|
|
|
47
47
|
readonly maxDownloadBytes?: number;
|
|
48
48
|
/** A claim not renewed for this long is taken over by another process. Defaults to 60 seconds. */
|
|
49
49
|
readonly claimStaleMs?: number;
|
|
50
|
+
/** Told when a task ends (completed, failed or lost). */
|
|
51
|
+
readonly onFinished?: (record: TaskRecord) => void;
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
54
|
* How a failed submission may be taken further.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibedev-si/dsh-vibedev",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "VibeDev 账号与模型:在 DeepSeek Harness 里登录 VibeDev,与 DeepSeek 账号并存,使用 VibeDev 网关的模型(Claude、GPT 等),并生成图片、视频、音乐和播客 · VibeDev account and models for DeepSeek Harness: sign in to VibeDev next to the DeepSeek account and use the VibeDev gateway's models (Claude, GPT and more), plus image, video, music and podcast generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|