bb-plugin-svaib-workplace 0.5.5 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist/app.meta.json +1 -1
- package/dist/host.js +28 -23
- package/dist/host.js.map +3 -3
- package/dist/host.meta.json +2 -2
- package/dist/server.js +37 -37
- package/dist/server.js.map +3 -3
- package/dist/server.meta.json +1 -1
- package/experiments/account-prototype/browser-host.ts +11 -3
- package/kimi-contract.ts +4 -2
- package/kimi-diagnostics.ts +42 -0
- package/kimi-host.ts +24 -11
- package/kimi-setup.ts +93 -19
- package/npm-release.json +3 -3
- package/package.json +6 -6
- package/scripts/setup-kimi-cc.d.mts +6 -3
- package/scripts/setup-kimi-cc.mjs +232 -25
package/dist/server.meta.json
CHANGED
|
@@ -4,12 +4,20 @@ import { browserContract } from './browser-contract';
|
|
|
4
4
|
import { nativeHookAutomatic } from './hook-native';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { inspectKimi, prepareKimi, replaceKimiKey } from '../../kimi-host';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { kimiDiagnostics, kimiDiagnosticSignals, kimiStep } from '../../kimi-diagnostics';
|
|
7
9
|
|
|
8
10
|
// The authorization URL never passes through a model, terminal or saved thread.
|
|
9
|
-
export default experimental_defineHostEntry({ contract: browserContract, handlers: {
|
|
11
|
+
export default experimental_defineHostEntry({ contract: browserContract, experimental_signals: kimiDiagnosticSignals, handlers: {
|
|
10
12
|
async kimiInspect(_, context) { return inspectKimi(join(context.experimental_paths.dataDir, 'kimi-cc'), context.signal); },
|
|
11
|
-
async kimiPrepare({ apiKey }, context) {
|
|
12
|
-
|
|
13
|
+
async kimiPrepare({ apiKey, diagnosticId }, context) {
|
|
14
|
+
const log = kimiDiagnostics(diagnosticId ?? randomUUID(), 'host', event => context.experimental_emitSignal('kimiDiagnostic', event));
|
|
15
|
+
return kimiStep(log, 'prepare', () => prepareKimi(join(context.experimental_paths.dataDir, 'kimi-cc'), apiKey, context.signal, log));
|
|
16
|
+
},
|
|
17
|
+
async kimiReplaceKey({ apiKey, diagnosticId }, context) {
|
|
18
|
+
const log = kimiDiagnostics(diagnosticId ?? randomUUID(), 'host', event => context.experimental_emitSignal('kimiDiagnostic', event));
|
|
19
|
+
await kimiStep(log, 'key-replace', () => replaceKimiKey(join(context.experimental_paths.dataDir, 'kimi-cc'), apiKey, context.signal, fetch, log)); return null;
|
|
20
|
+
},
|
|
13
21
|
async automaticHooks(input, context) { return nativeHookAutomatic(input, context.signal); },
|
|
14
22
|
async openLogin({ url }) {
|
|
15
23
|
const configured = process.env.BROWSER;
|
package/kimi-contract.ts
CHANGED
|
@@ -13,11 +13,12 @@ const key = z.string().max(512).regex(/^sk-kimi-[A-Za-z0-9_-]+$/, 'Нужен к
|
|
|
13
13
|
export const kimiHostStatusSchema = z.object({
|
|
14
14
|
nodeReady: z.boolean(), nodeVersion: z.string().nullable(),
|
|
15
15
|
profile: z.enum(['absent', 'partial', 'ready', 'blocked']), agent: kimiAgentSchema.nullable(),
|
|
16
|
+
verifyModels: z.array(z.string()).optional(),
|
|
16
17
|
}).strict();
|
|
17
18
|
export const kimiHostMethods = {
|
|
18
19
|
kimiInspect: { input: z.null(), output: kimiHostStatusSchema, experimental_retryable: true },
|
|
19
|
-
kimiPrepare: { input: z.object({ apiKey: key.optional() }).strict(), output: kimiAgentSchema },
|
|
20
|
-
kimiReplaceKey: { input: z.object({ apiKey: key }).strict(), output: z.null() },
|
|
20
|
+
kimiPrepare: { input: z.object({ apiKey: key.optional(), diagnosticId: z.uuid().optional() }).strict(), output: kimiAgentSchema },
|
|
21
|
+
kimiReplaceKey: { input: z.object({ apiKey: key, diagnosticId: z.uuid().optional() }).strict(), output: z.null() },
|
|
21
22
|
};
|
|
22
23
|
export const kimiViewSchema = z.object({
|
|
23
24
|
revision: z.number().int().nonnegative(),
|
|
@@ -31,6 +32,7 @@ export const kimiViewSchema = z.object({
|
|
|
31
32
|
checkToken: z.string().nullable().default(null),
|
|
32
33
|
checkStartedAt: z.number().nullable().default(null),
|
|
33
34
|
catalogVersion: z.number().int().nonnegative().default(0),
|
|
35
|
+
catalogCheckedAt: z.number().nullable().default(null),
|
|
34
36
|
automatic: z.boolean().default(false),
|
|
35
37
|
ownedTestThreadId: z.string().nullable().default(null),
|
|
36
38
|
}).strict();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// No free text: even a provider error containing credentials cannot enter the log.
|
|
4
|
+
export const kimiDiagnosticSchema = z.object({
|
|
5
|
+
diagnosticId: z.uuid(), source: z.enum(['backend', 'host']),
|
|
6
|
+
step: z.enum(['prepare', 'key-replace', 'key-commit', 'runtime', 'adapter-install', 'profile-inspect',
|
|
7
|
+
'profile-validate', 'profile-write', 'catalog-fetch', 'catalog-normalize', 'bb-discovery',
|
|
8
|
+
'registration', 'verification', 'refresh', 'migration', 'state', 'cleanup']),
|
|
9
|
+
outcome: z.enum(['start', 'success', 'failure', 'fallback', 'skipped']),
|
|
10
|
+
reason: z.enum(['setup', 'recheck', 'key-change', 'scheduled', 'startup', 'migration',
|
|
11
|
+
'cancelled', 'timeout', 'failed', 'http-error', 'network-error', 'invalid-json', 'oversized',
|
|
12
|
+
'invalid-catalog', 'cached', 'conservative', 'unavailable', 'foreign-profile', 'wrong-response']).optional(),
|
|
13
|
+
elapsedMs: z.number().int().nonnegative().optional(),
|
|
14
|
+
httpStatus: z.number().int().min(100).max(599).optional(),
|
|
15
|
+
byteCount: z.number().int().nonnegative().max(262144).optional(),
|
|
16
|
+
modelCount: z.number().int().nonnegative().max(129).optional(),
|
|
17
|
+
longModelCount: z.number().int().nonnegative().max(129).optional(),
|
|
18
|
+
profile: z.enum(['absent', 'partial', 'ready', 'blocked']).optional(),
|
|
19
|
+
stage: z.enum(['intro', 'node', 'key', 'installing', 'registration', 'verify', 'complete', 'blocked']).optional(),
|
|
20
|
+
phase: z.enum(['components', 'access', 'check']).nullable().optional(),
|
|
21
|
+
}).strict();
|
|
22
|
+
export type KimiDiagnostic = z.infer<typeof kimiDiagnosticSchema>;
|
|
23
|
+
export type KimiDiagnosticLog = (step: string, outcome: string, fields?: Record<string, unknown>) => void;
|
|
24
|
+
export const kimiDiagnosticSignals = { kimiDiagnostic: { payload: kimiDiagnosticSchema } };
|
|
25
|
+
|
|
26
|
+
export function kimiDiagnostics(diagnosticId: string, source: KimiDiagnostic['source'], sink: (event: KimiDiagnostic) => unknown): KimiDiagnosticLog {
|
|
27
|
+
return (step, outcome, fields = {}) => {
|
|
28
|
+
const parsed = kimiDiagnosticSchema.safeParse({ diagnosticId, source, step, outcome, ...fields });
|
|
29
|
+
if (!parsed.success) return;
|
|
30
|
+
try { void Promise.resolve(sink(parsed.data)).catch(() => {}); }
|
|
31
|
+
catch { /* Diagnostics must never fail the user's setup. */ }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function kimiStep<T>(log: KimiDiagnosticLog, step: string, work: () => Promise<T>): Promise<T> {
|
|
36
|
+
const started = Date.now(); log(step, 'start');
|
|
37
|
+
try { const result = await work(); log(step, 'success', { elapsedMs: Date.now() - started }); return result; }
|
|
38
|
+
catch (error) {
|
|
39
|
+
log(step, 'failure', { elapsedMs: Date.now() - started, reason: error instanceof Error && error.name === 'AbortError' ? 'cancelled' : 'failed' });
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
package/kimi-host.ts
CHANGED
|
@@ -2,9 +2,10 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { lstat, readFile, writeFile, rename, rm } from 'node:fs/promises';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { join, posix, win32 } from 'node:path';
|
|
5
|
-
import { provision, resumeProvision, registration, upgradeProvision, ADAPTER } from './scripts/setup-kimi-cc.mjs';
|
|
5
|
+
import { provision, resumeProvision, registration, upgradeProvision, validateProvision, ADAPTER } from './scripts/setup-kimi-cc.mjs';
|
|
6
6
|
import { kimiAgentSchema } from './kimi-contract';
|
|
7
7
|
import { ensureKimiRuntime, existingKimiRuntime } from './kimi-runtime';
|
|
8
|
+
import { kimiStep, type KimiDiagnosticLog } from './kimi-diagnostics';
|
|
8
9
|
|
|
9
10
|
/** Windows environment names are case-insensitive, including inside host workers. */
|
|
10
11
|
export function kimiCommandEnvironment(executable: string, environment: NodeJS.ProcessEnv = process.env, platform: string = process.platform) {
|
|
@@ -37,9 +38,9 @@ export function kimiCommand(executable: string, args: string[], signal: AbortSig
|
|
|
37
38
|
async function node(signal: AbortSignal) {
|
|
38
39
|
return existingKimiRuntime(signal);
|
|
39
40
|
}
|
|
40
|
-
async function regular(path: string, directory = false) {
|
|
41
|
+
async function regular(path: string, directory = false, maxBytes = 32_768) {
|
|
41
42
|
const info = await lstat(path);
|
|
42
|
-
if (info.isSymbolicLink() || (directory ? !info.isDirectory() : !info.isFile() || info.size >
|
|
43
|
+
if (info.isSymbolicLink() || (directory ? !info.isDirectory() : !info.isFile() || info.size > maxBytes)) throw new Error('Профиль настройки изменён. Ничего не перезаписано.');
|
|
43
44
|
}
|
|
44
45
|
export async function inspectKimi(root: string, signal: AbortSignal) {
|
|
45
46
|
const runtime = await node(signal);
|
|
@@ -49,7 +50,7 @@ export async function inspectKimi(root: string, signal: AbortSignal) {
|
|
|
49
50
|
return { ...base, profile: (error as NodeJS.ErrnoException).code === 'ENOENT' ? 'absent' as const : 'blocked' as const, agent: null };
|
|
50
51
|
}
|
|
51
52
|
try {
|
|
52
|
-
await regular(join(root, 'setup-owner.json'));
|
|
53
|
+
await regular(join(root, 'setup-owner.json'), false, 262_144);
|
|
53
54
|
const owner = JSON.parse(await readFile(join(root, 'setup-owner.json'), 'utf8'));
|
|
54
55
|
if (owner.schema !== 1 || owner.adapter !== ADAPTER || typeof owner.nodeExecutable !== 'string') throw new Error();
|
|
55
56
|
try { await regular(join(root, 'bb-agent.json')); }
|
|
@@ -59,32 +60,44 @@ export async function inspectKimi(root: string, signal: AbortSignal) {
|
|
|
59
60
|
}
|
|
60
61
|
const agent = kimiAgentSchema.parse(JSON.parse(await readFile(join(root, 'bb-agent.json'), 'utf8')));
|
|
61
62
|
if (![false, true, 'catalog' as const].some(legacy => JSON.stringify(agent) === JSON.stringify(registration(root, owner.nodeExecutable, legacy)))) throw new Error();
|
|
62
|
-
|
|
63
|
+
let verifyModels: string[] = [];
|
|
64
|
+
try {
|
|
65
|
+
const { catalog } = await validateProvision(root);
|
|
66
|
+
verifyModels = catalog.models.filter(model => !model.retired).map(model => model.id + (model.context >= 1_000_000 ? '[1m]' : ''));
|
|
67
|
+
} catch { /* An unrecognized profile cannot supply a trusted verification model. */ }
|
|
68
|
+
return { ...base, profile: 'ready' as const, agent, verifyModels };
|
|
63
69
|
} catch { return { ...base, profile: 'blocked' as const, agent: null }; }
|
|
64
70
|
}
|
|
65
|
-
export async function prepareKimi(root: string, apiKey: string | undefined, signal: AbortSignal) {
|
|
71
|
+
export async function prepareKimi(root: string, apiKey: string | undefined, signal: AbortSignal, log: KimiDiagnosticLog = () => {}) {
|
|
66
72
|
signal.throwIfAborted();
|
|
67
73
|
const status = await inspectKimi(root, signal);
|
|
68
|
-
|
|
74
|
+
log('profile-inspect', 'success', { profile: status.profile });
|
|
75
|
+
if (status.profile === 'ready') return upgradeProvision(root, { signal, log });
|
|
69
76
|
if (status.profile === 'blocked') throw new Error('Папка настройки изменена. Существующие файлы сохранены; нужна помощь партнёра.');
|
|
70
77
|
const install = async (executable: string, args: string[]) => { await kimiCommand(executable, args, signal); };
|
|
71
|
-
const runtime = await ensureKimiRuntime(root, signal);
|
|
72
|
-
if (status.profile === 'partial') return resumeProvision({ root, install, npmCli: runtime.npmCli, nodeExecutable: runtime.executable });
|
|
78
|
+
const runtime = await kimiStep(log, 'runtime', () => ensureKimiRuntime(root, signal));
|
|
79
|
+
if (status.profile === 'partial') return resumeProvision({ root, install, npmCli: runtime.npmCli, nodeExecutable: runtime.executable, signal, log });
|
|
73
80
|
if (!apiKey) throw new Error('Введи ключ из консоли Kimi Code.');
|
|
74
|
-
return provision({ root, apiKey, install, nodeExecutable: runtime.executable, npmCli: runtime.npmCli });
|
|
81
|
+
return provision({ root, apiKey, install, nodeExecutable: runtime.executable, npmCli: runtime.npmCli, signal, log });
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
export async function replaceKimiKey(root: string, apiKey: string, signal: AbortSignal) {
|
|
84
|
+
export async function replaceKimiKey(root: string, apiKey: string, signal: AbortSignal, fetchImpl: typeof fetch = fetch, log: KimiDiagnosticLog = () => {}) {
|
|
78
85
|
signal.throwIfAborted();
|
|
79
86
|
if (!/^sk-kimi-[A-Za-z0-9_-]+$/.test(apiKey)) throw new Error('Нужен ключ из консоли Kimi Code.');
|
|
80
87
|
const status = await inspectKimi(root, signal);
|
|
81
88
|
if (status.profile !== 'ready') throw new Error('Собственный профиль настройщика не найден.');
|
|
89
|
+
await validateProvision(root);
|
|
82
90
|
await regular(join(root, 'credentials.json'));
|
|
91
|
+
// Publish conservative settings while the old key is still in place. Every
|
|
92
|
+
// interruption after the key rename then leaves a recognized, safe profile.
|
|
93
|
+
await upgradeProvision(root, { signal, restrictContext: true, log });
|
|
83
94
|
signal.throwIfAborted();
|
|
84
95
|
const temporary = join(root, '.key-' + randomUUID());
|
|
85
96
|
try {
|
|
86
97
|
await writeFile(temporary, JSON.stringify({ apiKey }), { mode: 0o600, flag: 'wx' });
|
|
87
98
|
signal.throwIfAborted();
|
|
88
99
|
await rename(temporary, join(root, 'credentials.json'));
|
|
100
|
+
log('key-commit', 'success');
|
|
89
101
|
} finally { await rm(temporary, { force: true }); }
|
|
102
|
+
await upgradeProvision(root, { signal, fetchImpl, log });
|
|
90
103
|
}
|
package/kimi-setup.ts
CHANGED
|
@@ -2,10 +2,13 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import type { BbPluginApi } from '@get-bb/plugin-sdk';
|
|
3
3
|
import { browserContract } from './experiments/account-prototype/browser-contract';
|
|
4
4
|
import { kimiRpcContract, kimiViewSchema, type KimiAgent, type KimiView } from './kimi-contract';
|
|
5
|
+
import { kimiDiagnostics, kimiDiagnosticSignals, kimiStep, type KimiDiagnostic } from './kimi-diagnostics';
|
|
5
6
|
|
|
6
7
|
const PROVIDER = 'acp-claude-kimi';
|
|
7
8
|
const SETTINGS_PLUGIN = 'provider-acp';
|
|
8
9
|
const PREFERRED_MODEL = 'k3.1-pro';
|
|
10
|
+
const CATALOG_VERSION = 3;
|
|
11
|
+
const CATALOG_TTL = 24 * 60 * 60 * 1000;
|
|
9
12
|
async function bounded<T>(work: Promise<T>, ms = 30_000): Promise<T> {
|
|
10
13
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
11
14
|
try { return await Promise.race([work, new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error('BB timeout')), ms); })]); }
|
|
@@ -14,12 +17,22 @@ async function bounded<T>(work: Promise<T>, ms = 30_000): Promise<T> {
|
|
|
14
17
|
|
|
15
18
|
/** Owns only local Kimi UI state. It never reads/writes svaib onboarding marks. */
|
|
16
19
|
export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null) {
|
|
17
|
-
const client = bb.hosts.experimental_client({ contract: browserContract });
|
|
20
|
+
const client = bb.hosts.experimental_client({ contract: browserContract, experimental_signals: kimiDiagnosticSignals });
|
|
21
|
+
const writeDiagnostic = (event: KimiDiagnostic) => {
|
|
22
|
+
const line = '[kimi] ' + JSON.stringify(event);
|
|
23
|
+
try {
|
|
24
|
+
if (event.outcome === 'failure' || event.outcome === 'fallback') bb.log.warn(line); else bb.log.info(line);
|
|
25
|
+
} catch { /* Host signal delivery must also survive a failed log sink. */ }
|
|
26
|
+
};
|
|
27
|
+
const log = kimiDiagnostics(randomUUID(), 'backend', writeDiagnostic);
|
|
28
|
+
bb.onDispose(client.experimental_onSignal('kimiDiagnostic', event => {
|
|
29
|
+
if (event.hostId === hostId) writeDiagnostic(event.payload);
|
|
30
|
+
}));
|
|
18
31
|
const storageKey = 'kimi-setup:v1:' + (hostId ?? 'unavailable');
|
|
19
32
|
const saved = kimiViewSchema.safeParse(await bb.storage.kv.get(storageKey));
|
|
20
33
|
let view: KimiView = saved.success ? saved.data : {
|
|
21
34
|
revision: 0, canReplaceKey: false, hidden: false, stage: 'intro', message: '', hostId: hostId ?? '', testThreadId: null, verifyModel: null, manualRegistration: null, keyChangedAt: null,
|
|
22
|
-
phase: null, startedAt: null, checkToken: null, checkStartedAt: null, catalogVersion: 0, automatic: false, ownedTestThreadId: null,
|
|
35
|
+
phase: null, startedAt: null, checkToken: null, checkStartedAt: null, catalogVersion: 0, catalogCheckedAt: null, automatic: false, ownedTestThreadId: null,
|
|
23
36
|
};
|
|
24
37
|
let job: Promise<void> | null = null;
|
|
25
38
|
let starting: Promise<KimiView> | null = null;
|
|
@@ -33,8 +46,10 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
33
46
|
const snapshot = () => ({ ...view });
|
|
34
47
|
async function save(patch: Partial<KimiView>) {
|
|
35
48
|
if (disposed) return snapshot();
|
|
49
|
+
const changed = patch.stage !== undefined && patch.stage !== view.stage || patch.phase !== undefined && patch.phase !== view.phase;
|
|
36
50
|
view = { ...view, ...patch, revision: view.revision + 1 };
|
|
37
51
|
await bb.storage.kv.set(storageKey, view);
|
|
52
|
+
if (changed) log('state', 'success', { stage: view.stage, phase: view.phase });
|
|
38
53
|
bb.realtime.publish('kimi-setup-changed', null);
|
|
39
54
|
return snapshot();
|
|
40
55
|
}
|
|
@@ -42,6 +57,16 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
42
57
|
if (!hostId) throw new Error('Компьютер BB недоступен. Подключи его и повтори настройку.');
|
|
43
58
|
return { hostId, signal: abort.signal, timeoutMs: 600_000 };
|
|
44
59
|
}
|
|
60
|
+
async function hostMutation<T>(step: 'prepare' | 'key-replace', reason: KimiDiagnostic['reason'], work: (diagnosticId: string) => Promise<T>) {
|
|
61
|
+
const diagnosticId = randomUUID();
|
|
62
|
+
const operationLog = kimiDiagnostics(diagnosticId, 'backend', writeDiagnostic);
|
|
63
|
+
operationLog(step, 'start', { reason });
|
|
64
|
+
const started = Date.now();
|
|
65
|
+
try { const result = await work(diagnosticId); operationLog(step, 'success', { elapsedMs: Date.now() - started }); return result; }
|
|
66
|
+
catch (error) { operationLog(step, 'failure', { reason: abort.signal.aborted ? 'cancelled' : 'failed', elapsedMs: Date.now() - started }); throw error; }
|
|
67
|
+
}
|
|
68
|
+
const prepareProfile = (apiKey: string | undefined, reason: KimiDiagnostic['reason']) => hostMutation('prepare', reason,
|
|
69
|
+
diagnosticId => client.call('kimiPrepare', { ...(apiKey ? { apiKey } : {}), diagnosticId }, target()));
|
|
45
70
|
async function agents() {
|
|
46
71
|
const current = await bounded(bb.sdk.plugins.getSettings({ pluginId: SETTINGS_PLUGIN }));
|
|
47
72
|
if (!current.schema.customAgents || current.schema.customAgents.type !== 'string') throw new Error('ACP settings unavailable');
|
|
@@ -64,12 +89,20 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
64
89
|
/** Only seed models confirmed by this host's provider catalog. */
|
|
65
90
|
async function resolveVerifyModel(): Promise<string | null> {
|
|
66
91
|
if (!hostId) return null;
|
|
92
|
+
const started = Date.now(); log('bb-discovery', 'start');
|
|
67
93
|
try {
|
|
68
|
-
const catalog = await bounded(bb.sdk.providers.models({ hostId, providerId: PROVIDER }));
|
|
69
|
-
if (catalog.modelLoadError || !catalog.providers.some(provider => provider.id === PROVIDER && provider.available))
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
const catalog = await bounded(bb.sdk.providers.models({ hostId, providerId: PROVIDER, preferCached: false }));
|
|
95
|
+
if (catalog.modelLoadError || !catalog.providers.some(provider => provider.id === PROVIDER && provider.available)) {
|
|
96
|
+
log('bb-discovery', 'failure', { reason: 'unavailable', elapsedMs: Date.now() - started }); return null;
|
|
97
|
+
}
|
|
98
|
+
const status = await client.call('kimiInspect', null, target());
|
|
99
|
+
const allowed = status.verifyModels && status.agent && sameRegistration((await agents()).find(item => item.id === 'claude-kimi'), status.agent)
|
|
100
|
+
? new Set(status.verifyModels) : null;
|
|
101
|
+
const models = catalog.models.filter(model => (!model.routeProviderId || model.routeProviderId === PROVIDER) && (!allowed || allowed.has(model.id)));
|
|
102
|
+
const selected = (models.find(model => model.id === PREFERRED_MODEL) ?? models.find(model => model.id === 'k3.1-pro[1m]') ?? models.find(model => model.isDefault) ?? models[0])?.id ?? null;
|
|
103
|
+
log('bb-discovery', selected ? 'success' : 'failure', { modelCount: models.length, elapsedMs: Date.now() - started });
|
|
104
|
+
return selected;
|
|
105
|
+
} catch { log('bb-discovery', 'failure', { reason: 'failed', elapsedMs: Date.now() - started }); return null; }
|
|
73
106
|
}
|
|
74
107
|
async function inspect() {
|
|
75
108
|
if (await hasProvider()) {
|
|
@@ -108,11 +141,14 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
108
141
|
return sameRegistration(actual, agent);
|
|
109
142
|
}
|
|
110
143
|
async function register(agent: KimiAgent) {
|
|
144
|
+
log('registration', 'start');
|
|
111
145
|
try {
|
|
112
146
|
const owned = await writeRegistration(agent);
|
|
147
|
+
log('registration', owned ? 'success' : 'skipped', owned ? {} : { reason: 'foreign-profile' });
|
|
113
148
|
return save({ stage: 'verify', message: '', manualRegistration: null, verifyModel: await resolveVerifyModel(),
|
|
114
|
-
canReplaceKey: owned, ...(owned ? { catalogVersion:
|
|
149
|
+
canReplaceKey: owned, ...(owned ? { catalogVersion: CATALOG_VERSION, catalogCheckedAt: Date.now() } : {}) });
|
|
115
150
|
} catch {
|
|
151
|
+
log('registration', 'failure', { reason: 'failed' });
|
|
116
152
|
return save({ stage: 'registration', message: 'Профиль готов. В этой версии BB добавь провайдера через настройки ACP.', manualRegistration: JSON.stringify([agent], null, 2) });
|
|
117
153
|
}
|
|
118
154
|
}
|
|
@@ -125,7 +161,7 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
125
161
|
const run = async () => {
|
|
126
162
|
try {
|
|
127
163
|
if (status.agent && sameRegistration(current, status.agent)) {
|
|
128
|
-
const upgraded = await
|
|
164
|
+
const upgraded = await prepareProfile(undefined, 'recheck');
|
|
129
165
|
await register(upgraded);
|
|
130
166
|
} else await save({ stage: 'verify', canReplaceKey: false, verifyModel: await resolveVerifyModel() });
|
|
131
167
|
if (view.stage === 'verify') await beginAutomaticCheck();
|
|
@@ -142,7 +178,7 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
142
178
|
// a terminal, a model request, command argv, or the returned view.
|
|
143
179
|
const run = async () => {
|
|
144
180
|
try {
|
|
145
|
-
const agent = await
|
|
181
|
+
const agent = await prepareProfile(apiKey, 'setup');
|
|
146
182
|
apiKey = undefined;
|
|
147
183
|
if (!disposed) {
|
|
148
184
|
await save({ canReplaceKey: true, phase: 'access' });
|
|
@@ -198,10 +234,12 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
198
234
|
if ((error as { status?: number }).status === 404 || (error as { code?: string }).code === 'NOT_FOUND') {
|
|
199
235
|
if (view.ownedTestThreadId === id) await save({ ownedTestThreadId: null, checkToken: null, ...(view.testThreadId === id ? { testThreadId: null } : {}) }); return true;
|
|
200
236
|
}
|
|
237
|
+
log('cleanup', 'failure', { reason: 'failed' });
|
|
201
238
|
return false;
|
|
202
239
|
}
|
|
203
240
|
}
|
|
204
241
|
async function failAutomaticCheck(message: string, testId: string) {
|
|
242
|
+
log('verification', 'failure', { reason: 'unavailable', elapsedMs: Date.now() - (view.checkStartedAt ?? Date.now()) });
|
|
205
243
|
await save({ phase: null, message });
|
|
206
244
|
if (view.ownedTestThreadId === testId) {
|
|
207
245
|
try { await checkedThread(testId); await bb.sdk.threads.stop({ threadId: testId }); } catch { /* Retry keeps the owned ID for cleanup. */ }
|
|
@@ -242,6 +280,7 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
242
280
|
await save({ phase: 'check' }); watchAutomaticCheck(); return;
|
|
243
281
|
}
|
|
244
282
|
await save({ phase: 'check', stage: 'verify', checkStartedAt: Date.now(), checkToken: randomUUID(), message: '', testThreadId: null });
|
|
283
|
+
log('verification', 'start');
|
|
245
284
|
try {
|
|
246
285
|
const model = view.verifyModel;
|
|
247
286
|
if (!model || !hostId) throw new Error('model-unavailable');
|
|
@@ -259,6 +298,7 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
259
298
|
await save({ testThreadId: thread.id, ownedTestThreadId: thread.id });
|
|
260
299
|
watchAutomaticCheck();
|
|
261
300
|
} catch {
|
|
301
|
+
log('verification', 'failure', { reason: 'failed' });
|
|
262
302
|
try { await recoverOwnCheck(); } catch { /* Retry retains the nonce for recovery. */ }
|
|
263
303
|
if (view.ownedTestThreadId) watchAutomaticCheck();
|
|
264
304
|
else await save({ phase: null, message: 'Не удалось запустить проверку Kimi. Проверь подключение к BB и повтори.' });
|
|
@@ -275,7 +315,11 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
275
315
|
if (thread.status !== 'idle') return pending('Ответ ещё не получен. Проверь разговор и повтори проверку.');
|
|
276
316
|
const { output } = await bounded(bb.sdk.threads.output({ threadId: thread.id }));
|
|
277
317
|
if (!current()) return snapshot();
|
|
278
|
-
if (output?.trim() !== 'KIMI_READY')
|
|
318
|
+
if (output?.trim() !== 'KIMI_READY') {
|
|
319
|
+
if (!quiet) log('verification', 'failure', { reason: 'wrong-response' });
|
|
320
|
+
return pending('Проверочный ответ не подтверждён. Открой разговор и проверь результат.');
|
|
321
|
+
}
|
|
322
|
+
log('verification', 'success', { elapsedMs: Date.now() - (view.checkStartedAt ?? Date.now()) });
|
|
279
323
|
await save({ stage: 'complete', hidden: true, phase: null, message: 'Готово к работе', manualRegistration: null });
|
|
280
324
|
await cleanOwnCheck();
|
|
281
325
|
return snapshot();
|
|
@@ -339,10 +383,13 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
339
383
|
// old key must not complete setup while the key is being replaced.
|
|
340
384
|
await recoverOwnCheck();
|
|
341
385
|
if (!await cleanOwnCheck(false)) throw new Error('cleanup-pending');
|
|
342
|
-
|
|
343
|
-
|
|
386
|
+
// An RPC failure cannot prove whether the host committed the new key.
|
|
387
|
+
// Invalidate the old manual binding before crossing that boundary.
|
|
388
|
+
await save({ hidden: false, stage: 'installing', phase: 'access', startedAt: Date.now(),
|
|
389
|
+
keyChangedAt: Date.now(), testThreadId: null, verifyModel: null, message: '' });
|
|
390
|
+
await hostMutation('key-replace', 'key-change', diagnosticId => client.call('kimiReplaceKey', { apiKey, diagnosticId }, target()));
|
|
344
391
|
const keyChangedAt = Date.now();
|
|
345
|
-
await save({ stage: 'verify', phase: null, hidden: false, testThreadId: null, ownedTestThreadId: null, keyChangedAt,
|
|
392
|
+
await save({ stage: 'verify', phase: null, hidden: false, testThreadId: null, ownedTestThreadId: null, keyChangedAt, catalogCheckedAt: Date.now(), verifyModel: await resolveVerifyModel(), message: view.automatic ? '' : 'Ключ обновлён. Отправь новый проверочный запрос.' });
|
|
346
393
|
if (view.automatic) { await save({ startedAt: Date.now() }); await beginAutomaticCheck(); }
|
|
347
394
|
return snapshot();
|
|
348
395
|
} catch (error) {
|
|
@@ -351,21 +398,24 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
351
398
|
} finally { replacing = false; }
|
|
352
399
|
},
|
|
353
400
|
});
|
|
354
|
-
const needsResume = view.stage === 'complete' && view.catalogVersion <
|
|
401
|
+
const needsResume = view.stage === 'complete' && view.catalogVersion < CATALOG_VERSION && view.canReplaceKey
|
|
355
402
|
|| view.automatic && (view.phase === 'check' || view.stage === 'installing');
|
|
356
403
|
if (needsResume) bb.background.service('kimi-resume', {
|
|
357
404
|
async start(signal) {
|
|
358
405
|
if (signal.aborted || disposed) return;
|
|
359
406
|
// Host calls are forbidden during factory registration. Services start
|
|
360
407
|
// only once BB has registered the plugin and its host entry.
|
|
361
|
-
if (view.stage === 'complete' && view.catalogVersion <
|
|
362
|
-
//
|
|
408
|
+
if (view.stage === 'complete' && view.catalogVersion < CATALOG_VERSION && view.canReplaceKey) {
|
|
409
|
+
// Refresh owned context limits without another model request or reinstall.
|
|
363
410
|
job = (async () => {
|
|
364
411
|
try {
|
|
365
412
|
const status = await client.call('kimiInspect', null, target());
|
|
366
413
|
if (!status.agent || !sameRegistration((await agents()).find(item => item.id === 'claude-kimi'), status.agent)) return;
|
|
367
|
-
const agent = await
|
|
368
|
-
if (await writeRegistration(agent))
|
|
414
|
+
const agent = await prepareProfile(undefined, 'migration');
|
|
415
|
+
if (await writeRegistration(agent)) {
|
|
416
|
+
if (!await resolveVerifyModel()) throw new Error('Kimi catalog discovery failed');
|
|
417
|
+
await save({ automatic: true, catalogVersion: CATALOG_VERSION, catalogCheckedAt: Date.now(), message: 'Готово к работе' });
|
|
418
|
+
}
|
|
369
419
|
} catch { bb.log.warn('Не удалось обновить настройки моделей Kimi. Работающее подключение сохранено; повтори проверку в настройках плагина.'); }
|
|
370
420
|
})().finally(() => { job = null; }); void job.catch(() => {});
|
|
371
421
|
}
|
|
@@ -385,5 +435,29 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
|
|
|
385
435
|
});
|
|
386
436
|
},
|
|
387
437
|
});
|
|
438
|
+
async function refreshCatalog(reason: 'scheduled' | 'startup' = 'scheduled') {
|
|
439
|
+
if (disposed || occupied() || !hostId || view.stage !== 'complete' || !view.canReplaceKey
|
|
440
|
+
|| view.catalogCheckedAt !== null && Date.now() - view.catalogCheckedAt < CATALOG_TTL) return;
|
|
441
|
+
job = kimiStep(log, 'refresh', async () => {
|
|
442
|
+
const status = await client.call('kimiInspect', null, target());
|
|
443
|
+
if (!status.agent || !sameRegistration((await agents()).find(item => item.id === 'claude-kimi'), status.agent)) return;
|
|
444
|
+
const agent = await prepareProfile(undefined, reason);
|
|
445
|
+
if (!await writeRegistration(agent)) return;
|
|
446
|
+
// A failed BB discovery must remain due for retry, even if host settings succeeded.
|
|
447
|
+
if (!await resolveVerifyModel()) throw new Error('Kimi catalog discovery failed');
|
|
448
|
+
await save({ catalogVersion: CATALOG_VERSION, catalogCheckedAt: Date.now() });
|
|
449
|
+
}).finally(() => { job = null; });
|
|
450
|
+
await job;
|
|
451
|
+
}
|
|
452
|
+
// Hourly inexpensive TTL check; at most one catalog request per day. On a
|
|
453
|
+
// restart after downtime, refresh immediately once the host entry is available.
|
|
454
|
+
bb.background.schedule('kimi-catalog-refresh', '0 * * * *', () => refreshCatalog('scheduled'));
|
|
455
|
+
if (!needsResume) bb.background.service('kimi-catalog-refresh', {
|
|
456
|
+
async start(signal) {
|
|
457
|
+
try { if (!signal.aborted) await refreshCatalog('startup'); }
|
|
458
|
+
catch { bb.log.warn('Не удалось обновить каталог Kimi. Повторим проверку по расписанию.'); }
|
|
459
|
+
if (!signal.aborted) await new Promise<void>(resolve => signal.addEventListener('abort', () => resolve(), { once: true }));
|
|
460
|
+
},
|
|
461
|
+
});
|
|
388
462
|
return { settled: async () => { await starting; await job; } };
|
|
389
463
|
}
|
package/npm-release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repository": "https://github.com/SolomonikVik/svaib-bb",
|
|
3
|
-
"tag": "v0.5.
|
|
4
|
-
"commit": "
|
|
5
|
-
"pluginVersion": "0.5.
|
|
3
|
+
"tag": "v0.5.6",
|
|
4
|
+
"commit": "bdf865e0df4f2c1f854f140299b02619ccf5c864",
|
|
5
|
+
"pluginVersion": "0.5.6",
|
|
6
6
|
"methodVersion": "0.11.0"
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bb-plugin-svaib-workplace",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
|
-
"bb": ">=0.
|
|
7
|
-
"bbPluginSdk": ">=0.6.
|
|
6
|
+
"bb": ">=0.49.0",
|
|
7
|
+
"bbPluginSdk": ">=0.6.132 <0.7"
|
|
8
8
|
},
|
|
9
9
|
"bb": {
|
|
10
10
|
"name": "svaib [Early access]",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
]
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"
|
|
24
|
-
"zod": "^4.6.5",
|
|
23
|
+
"@get-bb/plugin-sdk": "0.6.132",
|
|
25
24
|
"bb-plugin-svaib-screen-pilot": "file:./components/screen",
|
|
26
|
-
"
|
|
25
|
+
"echarts": "6.1.0",
|
|
26
|
+
"zod": "^4.6.5"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@testing-library/react": "^16.3.2",
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { KimiAgent } from '../kimi-contract';
|
|
2
|
+
import type { KimiDiagnosticLog } from '../kimi-diagnostics';
|
|
2
3
|
type Install = (executable: string, args: string[], options?: object) => Promise<void>;
|
|
4
|
+
type DiscoveryOptions = { fetchImpl?: typeof fetch; signal?: AbortSignal; log?: KimiDiagnosticLog };
|
|
3
5
|
export const ADAPTER: string;
|
|
4
6
|
export function npmCliPath(nodeExecutable?: string): Promise<string>;
|
|
5
7
|
export function registration(root: string, executable?: string, legacy?: boolean | 'catalog'): KimiAgent;
|
|
6
|
-
export function provision(options: { root: string; apiKey: string; install?: Install; npmCli?: string; nodeExecutable?: string }): Promise<KimiAgent>;
|
|
7
|
-
export function resumeProvision(options: { root: string; install?: Install; npmCli?: string; nodeExecutable?: string }): Promise<KimiAgent>;
|
|
8
|
+
export function provision(options: { root: string; apiKey: string; install?: Install; npmCli?: string; nodeExecutable?: string } & DiscoveryOptions): Promise<KimiAgent>;
|
|
9
|
+
export function resumeProvision(options: { root: string; install?: Install; npmCli?: string; nodeExecutable?: string } & DiscoveryOptions): Promise<KimiAgent>;
|
|
8
10
|
|
|
9
11
|
export const MODELS: string[][];
|
|
10
|
-
export function upgradeProvision(root: string): Promise<KimiAgent>;
|
|
12
|
+
export function upgradeProvision(root: string, options?: DiscoveryOptions & { restrictContext?: boolean }): Promise<KimiAgent>;
|
|
13
|
+
export function validateProvision(root: string): Promise<{ catalog: { models: { id: string; context: number; retired: boolean }[] } }>;
|