@steerable/agent-shell 0.6.58 → 0.6.60
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.
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek / Kimi 账户余额。
|
|
3
|
+
*
|
|
4
|
+
* 只打这两家的官方余额地址,用已保存的 API Key 做 Bearer。地址是常量,
|
|
5
|
+
* 不采用用户填写的 Base URL,避免密钥被送到任意主机。响应里不带回密钥。
|
|
6
|
+
*/
|
|
7
|
+
export interface LlmAccount {
|
|
8
|
+
status: 'ready' | 'unsupported' | 'missing_key' | 'failed';
|
|
9
|
+
provider: 'deepseek' | 'moonshot' | null;
|
|
10
|
+
label: string;
|
|
11
|
+
available: boolean | null;
|
|
12
|
+
currency: string | null;
|
|
13
|
+
total: string | null;
|
|
14
|
+
/** DeepSeek 赠送余额,或 Kimi 代金券余额。 */
|
|
15
|
+
granted: string | null;
|
|
16
|
+
/** DeepSeek 充值余额,或 Kimi 现金余额。 */
|
|
17
|
+
toppedUp: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface ReadLlmAccountOptions {
|
|
20
|
+
refresh?: boolean;
|
|
21
|
+
fetchImpl?: typeof fetch;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
interface AccountEndpoint {
|
|
25
|
+
provider: 'deepseek' | 'moonshot';
|
|
26
|
+
label: string;
|
|
27
|
+
url: string;
|
|
28
|
+
/** 余额响应本身不带币种时使用(Kimi)。DeepSeek 以响应里的 currency 为准。 */
|
|
29
|
+
currency: 'CNY' | 'USD' | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function resetLlmAccountCache(): void;
|
|
32
|
+
/** 设置页选中的厂商优先;没选时再看 Base URL 是不是这两家的官方主机。 */
|
|
33
|
+
export declare function accountEndpointFor(settings: {
|
|
34
|
+
vendorId?: string;
|
|
35
|
+
baseUrl?: string;
|
|
36
|
+
}): AccountEndpoint | null;
|
|
37
|
+
export declare function readLlmAccount(settings: {
|
|
38
|
+
vendorId?: string;
|
|
39
|
+
baseUrl?: string;
|
|
40
|
+
apiKey?: string;
|
|
41
|
+
}, options?: ReadLlmAccountOptions): Promise<LlmAccount>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek / Kimi 账户余额。
|
|
3
|
+
*
|
|
4
|
+
* 只打这两家的官方余额地址,用已保存的 API Key 做 Bearer。地址是常量,
|
|
5
|
+
* 不采用用户填写的 Base URL,避免密钥被送到任意主机。响应里不带回密钥。
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
const DEEPSEEK = {
|
|
9
|
+
provider: 'deepseek',
|
|
10
|
+
label: 'DeepSeek',
|
|
11
|
+
url: 'https://api.deepseek.com/user/balance',
|
|
12
|
+
currency: null,
|
|
13
|
+
};
|
|
14
|
+
const MOONSHOT_CN = {
|
|
15
|
+
provider: 'moonshot',
|
|
16
|
+
label: 'Kimi',
|
|
17
|
+
url: 'https://api.moonshot.cn/v1/users/me/balance',
|
|
18
|
+
currency: 'CNY',
|
|
19
|
+
};
|
|
20
|
+
const MOONSHOT_AI = {
|
|
21
|
+
provider: 'moonshot',
|
|
22
|
+
label: 'Kimi',
|
|
23
|
+
url: 'https://api.moonshot.ai/v1/users/me/balance',
|
|
24
|
+
currency: 'USD',
|
|
25
|
+
};
|
|
26
|
+
const READY_TTL_MS = 30_000;
|
|
27
|
+
const FAILED_TTL_MS = 15_000;
|
|
28
|
+
let cache = null;
|
|
29
|
+
export function resetLlmAccountCache() {
|
|
30
|
+
cache = null;
|
|
31
|
+
}
|
|
32
|
+
function empty(status, endpoint) {
|
|
33
|
+
return {
|
|
34
|
+
status,
|
|
35
|
+
provider: endpoint?.provider ?? null,
|
|
36
|
+
label: endpoint?.label ?? '',
|
|
37
|
+
available: null,
|
|
38
|
+
currency: null,
|
|
39
|
+
total: null,
|
|
40
|
+
granted: null,
|
|
41
|
+
toppedUp: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function failed(endpoint) {
|
|
45
|
+
return { ...empty('missing_key', endpoint), status: 'failed' };
|
|
46
|
+
}
|
|
47
|
+
function hostnameOf(baseUrl) {
|
|
48
|
+
if (!baseUrl)
|
|
49
|
+
return '';
|
|
50
|
+
try {
|
|
51
|
+
return new URL(baseUrl).hostname.toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** 设置页选中的厂商优先;没选时再看 Base URL 是不是这两家的官方主机。 */
|
|
58
|
+
export function accountEndpointFor(settings) {
|
|
59
|
+
const vendor = settings.vendorId?.trim().toLowerCase() ?? '';
|
|
60
|
+
if (vendor === 'moonshotai')
|
|
61
|
+
return MOONSHOT_AI;
|
|
62
|
+
if (vendor === 'moonshotai-cn' || vendor === 'moonshot' || vendor === 'kimi')
|
|
63
|
+
return MOONSHOT_CN;
|
|
64
|
+
if (vendor === 'deepseek')
|
|
65
|
+
return DEEPSEEK;
|
|
66
|
+
const host = hostnameOf(settings.baseUrl);
|
|
67
|
+
if (host === 'api.moonshot.ai')
|
|
68
|
+
return MOONSHOT_AI;
|
|
69
|
+
if (host === 'api.moonshot.cn')
|
|
70
|
+
return MOONSHOT_CN;
|
|
71
|
+
if (host === 'api.deepseek.com')
|
|
72
|
+
return DEEPSEEK;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function moneyString(value) {
|
|
76
|
+
if (typeof value === 'string') {
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
if (!/^-?\d+(\.\d+)?$/.test(trimmed))
|
|
79
|
+
return null;
|
|
80
|
+
return trimmed;
|
|
81
|
+
}
|
|
82
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
83
|
+
return value.toFixed(4).replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '');
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function asRecord(value) {
|
|
88
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
89
|
+
return null;
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function pickCurrency(rows) {
|
|
93
|
+
const rank = (row) => {
|
|
94
|
+
const amount = Number(row.total);
|
|
95
|
+
const positive = Number.isFinite(amount) && amount > 0 ? 2 : 0;
|
|
96
|
+
const currency = row.currency === 'CNY' ? 2 : row.currency === 'USD' ? 1 : 0;
|
|
97
|
+
return positive + currency;
|
|
98
|
+
};
|
|
99
|
+
return [...rows].sort((a, b) => rank(b) - rank(a))[0];
|
|
100
|
+
}
|
|
101
|
+
function parseDeepseek(body) {
|
|
102
|
+
const record = asRecord(body);
|
|
103
|
+
if (!record || !Array.isArray(record.balance_infos))
|
|
104
|
+
return null;
|
|
105
|
+
const rows = [];
|
|
106
|
+
for (const item of record.balance_infos) {
|
|
107
|
+
const row = asRecord(item);
|
|
108
|
+
if (!row || typeof row.currency !== 'string')
|
|
109
|
+
continue;
|
|
110
|
+
const total = moneyString(row.total_balance);
|
|
111
|
+
if (!total)
|
|
112
|
+
continue;
|
|
113
|
+
rows.push({
|
|
114
|
+
currency: row.currency,
|
|
115
|
+
total,
|
|
116
|
+
granted: moneyString(row.granted_balance),
|
|
117
|
+
toppedUp: moneyString(row.topped_up_balance),
|
|
118
|
+
available: typeof record.is_available === 'boolean' ? record.is_available : null,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (rows.length === 0)
|
|
122
|
+
return null;
|
|
123
|
+
return pickCurrency(rows);
|
|
124
|
+
}
|
|
125
|
+
function parseMoonshot(body, currency) {
|
|
126
|
+
const record = asRecord(body);
|
|
127
|
+
if (!record)
|
|
128
|
+
return null;
|
|
129
|
+
const data = asRecord(record.data) ?? record;
|
|
130
|
+
const total = moneyString(data.available_balance);
|
|
131
|
+
if (!total)
|
|
132
|
+
return null;
|
|
133
|
+
const amount = Number(total);
|
|
134
|
+
return {
|
|
135
|
+
currency,
|
|
136
|
+
total,
|
|
137
|
+
granted: moneyString(data.voucher_balance),
|
|
138
|
+
toppedUp: moneyString(data.cash_balance),
|
|
139
|
+
available: Number.isFinite(amount) ? amount > 0 : null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function fingerprint(endpoint, apiKey) {
|
|
143
|
+
return createHash('sha256').update(`${endpoint.url}\n${apiKey}`).digest('hex');
|
|
144
|
+
}
|
|
145
|
+
async function fetchBalance(endpoint, apiKey, fetchImpl) {
|
|
146
|
+
let response;
|
|
147
|
+
try {
|
|
148
|
+
response = await fetchImpl(endpoint.url, {
|
|
149
|
+
method: 'GET',
|
|
150
|
+
redirect: 'manual',
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${apiKey}`,
|
|
153
|
+
Accept: 'application/json',
|
|
154
|
+
},
|
|
155
|
+
signal: AbortSignal.timeout(8_000),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
console.info(`[llm] account ${endpoint.provider} unreachable: ${err instanceof Error ? err.message : String(err)}`);
|
|
160
|
+
return failed(endpoint);
|
|
161
|
+
}
|
|
162
|
+
if (response.status !== 200) {
|
|
163
|
+
console.info(`[llm] account ${endpoint.provider} status ${response.status}`);
|
|
164
|
+
return failed(endpoint);
|
|
165
|
+
}
|
|
166
|
+
let body;
|
|
167
|
+
try {
|
|
168
|
+
const text = await response.text();
|
|
169
|
+
if (text.length > 65_536)
|
|
170
|
+
return failed(endpoint);
|
|
171
|
+
body = JSON.parse(text);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return failed(endpoint);
|
|
175
|
+
}
|
|
176
|
+
const parsed = endpoint.provider === 'deepseek'
|
|
177
|
+
? parseDeepseek(body)
|
|
178
|
+
: parseMoonshot(body, endpoint.currency ?? 'CNY');
|
|
179
|
+
if (!parsed)
|
|
180
|
+
return failed(endpoint);
|
|
181
|
+
return {
|
|
182
|
+
status: 'ready',
|
|
183
|
+
provider: endpoint.provider,
|
|
184
|
+
label: endpoint.label,
|
|
185
|
+
available: parsed.available,
|
|
186
|
+
currency: parsed.currency,
|
|
187
|
+
total: parsed.total,
|
|
188
|
+
granted: parsed.granted,
|
|
189
|
+
toppedUp: parsed.toppedUp,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
export async function readLlmAccount(settings, options = {}) {
|
|
193
|
+
const endpoint = accountEndpointFor(settings);
|
|
194
|
+
if (!endpoint)
|
|
195
|
+
return empty('unsupported', null);
|
|
196
|
+
const apiKey = settings.apiKey?.trim() ?? '';
|
|
197
|
+
if (!apiKey)
|
|
198
|
+
return empty('missing_key', endpoint);
|
|
199
|
+
const now = options.now ?? Date.now;
|
|
200
|
+
const id = fingerprint(endpoint, apiKey);
|
|
201
|
+
const cached = cache;
|
|
202
|
+
if (!options.refresh && cached && cached.fingerprint === id) {
|
|
203
|
+
const ttl = cached.value.status === 'ready' ? READY_TTL_MS : FAILED_TTL_MS;
|
|
204
|
+
if (now() - cached.at < ttl)
|
|
205
|
+
return cached.value;
|
|
206
|
+
}
|
|
207
|
+
const account = await fetchBalance(endpoint, apiKey, options.fetchImpl ?? fetch);
|
|
208
|
+
cache = { fingerprint: id, at: now(), value: account };
|
|
209
|
+
return account;
|
|
210
|
+
}
|
|
@@ -28,6 +28,7 @@ import { clampExecPolicy } from '../host-tools.js';
|
|
|
28
28
|
import { getResolvedHostTools, isProductLlmLocked, resolveRuntimeLlmSettings, resolveTurnApproval, resolveTurnChatMode, resolveTurnOrchestration, } from '../host-tools-runtime.js';
|
|
29
29
|
import { SidecarSupervisor } from '../sidecar/index.js';
|
|
30
30
|
import { diagnoseLlmConnection } from './llm-diagnose.js';
|
|
31
|
+
import { readLlmAccount } from './llm-account.js';
|
|
31
32
|
import { brandSkillVars, buildSystemPrompt, buildForcedSkillMessage, conditionsFromTools, } from './prompt-builder.js';
|
|
32
33
|
import { parseUserMessageTriggers } from './message-triggers.js';
|
|
33
34
|
import { findSkill, loadSkills, classifySkillOrigin, listSkillRoots } from './skill-loader.js';
|
|
@@ -1576,6 +1577,13 @@ export class LocalBackendRouter {
|
|
|
1576
1577
|
}
|
|
1577
1578
|
return { status: 200, data: { posture: supervisor.getSandboxPosture(), egress } };
|
|
1578
1579
|
}
|
|
1580
|
+
if (method === 'GET' && pathname === '/api/v2/llm/account') {
|
|
1581
|
+
const settings = resolveRuntimeLlmSettings(await this.store.getLlmSettings());
|
|
1582
|
+
const account = await readLlmAccount(settings, {
|
|
1583
|
+
refresh: url.searchParams.get('refresh') === '1',
|
|
1584
|
+
});
|
|
1585
|
+
return { status: 200, data: account };
|
|
1586
|
+
}
|
|
1579
1587
|
if (pathname === '/api/v2/local-settings/llm') {
|
|
1580
1588
|
if (method === 'GET') {
|
|
1581
1589
|
return {
|
|
@@ -147,6 +147,7 @@ export function downloadPublishedEgress(repoRoot, cacheDir) {
|
|
|
147
147
|
const child = spawn(python, [script, 'egress', '--artifact-lock', '--target', 'host', '--out', cacheDir], {
|
|
148
148
|
cwd: repoRoot,
|
|
149
149
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
150
|
+
windowsHide: true,
|
|
150
151
|
});
|
|
151
152
|
const timer = setTimeout(() => child.kill(), EGRESS_DOWNLOAD_TIMEOUT_MS);
|
|
152
153
|
let stdout = '';
|
|
@@ -428,6 +429,7 @@ export async function allowEgressForBaseUrl(baseUrl) {
|
|
|
428
429
|
export async function startEgressProxy(plan, onLog, readyTimeoutMs = 10_000) {
|
|
429
430
|
const child = spawn(plan.command, plan.args, {
|
|
430
431
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
432
|
+
windowsHide: true,
|
|
431
433
|
// 凭证与控制 token 经 env 传入代理进程——argv 在 ps 里可见,env 不入
|
|
432
434
|
// 命令行。
|
|
433
435
|
env: plan.broker
|
|
@@ -389,6 +389,7 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
389
389
|
cwd: this.options.cwd,
|
|
390
390
|
env: { ...process.env, ...this.options.env, ...spawnPlan.env },
|
|
391
391
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
392
|
+
windowsHide: true,
|
|
392
393
|
});
|
|
393
394
|
this.child = child;
|
|
394
395
|
this.attachListeners(child);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.60",
|
|
4
4
|
"description": "Steerable framework — product-neutral Node HostRuntime and headless HTTP server shared by Tauri desktop and browser deployments; scenario packs extend it through @steerable/pack-sdk.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"dotenv": "^17.4.2",
|
|
52
52
|
"he": "^1.2.0",
|
|
53
53
|
"node-pty": "^1.1.0",
|
|
54
|
-
"@steerable/agent-harness": "0.6.
|
|
55
|
-
"@steerable/agent-protocol": "0.6.
|
|
56
|
-
"@steerable/pack-sdk": "0.6.
|
|
54
|
+
"@steerable/agent-harness": "0.6.60",
|
|
55
|
+
"@steerable/agent-protocol": "0.6.60",
|
|
56
|
+
"@steerable/pack-sdk": "0.6.60"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/better-sqlite3": "^7.6.13",
|