merge: integrate remote and local main histories
# Conflicts: # .project-docs/20-architecture/module-map.md # .project-docs/30-worklog/current-state.md # .project-docs/50-evidence/evidence-index.md # src/pages/MyPlugins/index.tsx # tests/unit/pi-agent-server-process-real.test.ts # tests/unit/pi-managed-worker-opener.test.ts # tests/unit/plugin-marketplace-pages.test.tsx
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
} from '../route-utils';
|
||||
import { decodeRouteId, sendCodingRouteError } from './coding-route-errors';
|
||||
|
||||
const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high']);
|
||||
const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'max']);
|
||||
|
||||
function invalidRequest(message: string): never {
|
||||
throw new CodingConversationServiceError(400, 'CODING_CONVERSATION_REQUEST_INVALID', message);
|
||||
@@ -187,7 +187,7 @@ export async function handleCodingConversationRoutes(
|
||||
sendJson(res, 200, {
|
||||
model: await service.setThinking(
|
||||
conversationId,
|
||||
body.thinkingLevel as 'off' | 'minimal' | 'low' | 'medium' | 'high',
|
||||
body.thinkingLevel as 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'max',
|
||||
),
|
||||
});
|
||||
return true;
|
||||
|
||||
112
electron/api/routes/device-packages.ts
Normal file
112
electron/api/routes/device-packages.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { DevicePackageError } from '../../coding-packages/device-package-manager';
|
||||
import type { HostApiContext } from '../context';
|
||||
import { parseJsonBody, sendJson } from '../route-utils';
|
||||
|
||||
const ROOT = '/api/coding/device-packages';
|
||||
const PACKAGE = /^\/api\/coding\/device-packages\/([^/]+)$/u;
|
||||
const PACKAGE_ID = /^[a-z0-9][a-z0-9._-]{0,127}$/u;
|
||||
|
||||
class DevicePackageRouteError extends Error {
|
||||
constructor(readonly status: 400 | 404, message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
function packageId(value: string): string {
|
||||
try {
|
||||
const decoded = decodeURIComponent(value);
|
||||
if (PACKAGE_ID.test(decoded)) return decoded;
|
||||
} catch {
|
||||
// Project a single bounded request error below.
|
||||
}
|
||||
throw new DevicePackageRouteError(400, 'Device package id is invalid');
|
||||
}
|
||||
|
||||
function noQuery(url: URL): void {
|
||||
if (url.search) throw new DevicePackageRouteError(400, 'Query parameters are not supported');
|
||||
}
|
||||
|
||||
async function exactBody(req: IncomingMessage, keys: readonly string[]): Promise<Record<string, unknown>> {
|
||||
const body = await parseJsonBody<unknown>(req);
|
||||
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
||||
throw new DevicePackageRouteError(400, 'Request body is invalid');
|
||||
}
|
||||
const record = body as Record<string, unknown>;
|
||||
const actual = Object.keys(record).sort();
|
||||
const expected = [...keys].sort();
|
||||
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
||||
throw new DevicePackageRouteError(400, 'Request body has unexpected fields');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
function sendError(res: ServerResponse, error: unknown): void {
|
||||
if (error instanceof DevicePackageRouteError) {
|
||||
sendJson(res, error.status, {
|
||||
success: false, code: 'local_package_request_invalid', error: error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (error instanceof DevicePackageError) {
|
||||
const status = error.code === 'local_package_not_installed' || error.code === 'local_package_not_found'
|
||||
? 404
|
||||
: error.code === 'local_package_in_use' || error.code === 'local_package_confirmation_required'
|
||||
? 409
|
||||
: error.code === 'local_package_dependency_failed' || error.code === 'local_package_install_failed'
|
||||
? 503
|
||||
: 422;
|
||||
sendJson(res, status, { success: false, code: error.code, error: error.message });
|
||||
return;
|
||||
}
|
||||
if (error instanceof SyntaxError) {
|
||||
sendJson(res, 400, {
|
||||
success: false, code: 'local_package_request_invalid', error: 'Request body is invalid',
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendJson(res, 503, {
|
||||
success: false, code: 'local_package_install_failed', error: 'Device package service is unavailable',
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleDevicePackageRoutes(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
url: URL,
|
||||
ctx: HostApiContext,
|
||||
): Promise<boolean> {
|
||||
const match = PACKAGE.exec(url.pathname);
|
||||
const known = (url.pathname === ROOT && req.method === 'GET')
|
||||
|| Boolean(match && (req.method === 'PATCH' || req.method === 'DELETE'));
|
||||
if (!known) return false;
|
||||
const manager = ctx.codingProducts?.devicePackages;
|
||||
if (!manager) {
|
||||
sendJson(res, 503, {
|
||||
success: false, code: 'local_package_install_failed', error: 'Device package service is unavailable',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
noQuery(url);
|
||||
if (url.pathname === ROOT) {
|
||||
sendJson(res, 200, await manager.list());
|
||||
return true;
|
||||
}
|
||||
const id = packageId(match?.[1] ?? '');
|
||||
if (req.method === 'PATCH') {
|
||||
const body = await exactBody(req, ['enabled']);
|
||||
if (typeof body.enabled !== 'boolean') {
|
||||
throw new DevicePackageRouteError(400, 'enabled is invalid');
|
||||
}
|
||||
sendJson(res, 200, await manager.setEnabled(id, body.enabled));
|
||||
return true;
|
||||
}
|
||||
await exactBody(req, []);
|
||||
sendJson(res, 200, await manager.uninstall(id));
|
||||
return true;
|
||||
} catch (error) {
|
||||
sendError(res, error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,10 @@ import { seedWorksSquareAIGatewayCredential } from '../../services/works-square-
|
||||
import {
|
||||
NIANCODE_USER_MODEL_ACCOUNT_ID,
|
||||
NIANCODE_USER_MODEL_ACCOUNT_LABEL,
|
||||
normalizeImportedModelCapabilities,
|
||||
normalizeImportedUserModelId,
|
||||
} from '../../../shared/user-model-config';
|
||||
import type { ImportedModelCapabilities } from '../../../shared/imported-model-profile';
|
||||
|
||||
const legacyProviderRoutesWarned = new Set<string>();
|
||||
|
||||
@@ -141,6 +143,7 @@ type ImportedUserModelConfig = {
|
||||
credentialMode: string;
|
||||
apiKeyExpiresIn: number | null;
|
||||
models: string[];
|
||||
modelCapabilities?: ImportedModelCapabilities;
|
||||
};
|
||||
|
||||
const WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE = 'works_square_ai_gateway';
|
||||
@@ -157,7 +160,7 @@ class WorksSquareModelConfigError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeImportedUserModelConfig(payload: unknown): ImportedUserModelConfig {
|
||||
export function normalizeImportedUserModelConfig(payload: unknown): ImportedUserModelConfig {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
throw new Error('Works Square model config response is invalid');
|
||||
}
|
||||
@@ -181,6 +184,7 @@ function normalizeImportedUserModelConfig(payload: unknown): ImportedUserModelCo
|
||||
const label = typeof record.label === 'string' && record.label.trim()
|
||||
? record.label.trim()
|
||||
: NIANCODE_USER_MODEL_ACCOUNT_LABEL;
|
||||
const modelCapabilities = normalizeImportedModelCapabilities(record.model_capabilities, models);
|
||||
|
||||
return {
|
||||
label,
|
||||
@@ -189,6 +193,7 @@ function normalizeImportedUserModelConfig(payload: unknown): ImportedUserModelCo
|
||||
credentialMode: credentialMode || 'api_key',
|
||||
apiKeyExpiresIn,
|
||||
models,
|
||||
...(modelCapabilities ? { modelCapabilities } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -211,9 +216,13 @@ function importedUserModelMetadata(
|
||||
if (useLocalAiProxy) {
|
||||
const metadata = { ...(existing?.metadata ?? {}) };
|
||||
delete metadata.worksSquareCredentialExpiresAt;
|
||||
delete metadata.worksSquareModelCapabilities;
|
||||
return {
|
||||
...metadata,
|
||||
customModels: modelConfig.models,
|
||||
...(modelConfig.modelCapabilities
|
||||
? { worksSquareModelCapabilities: modelConfig.modelCapabilities }
|
||||
: {}),
|
||||
worksSquareCredentialMode: WORKS_SQUARE_AI_GATEWAY_PROXY_CREDENTIAL_MODE,
|
||||
worksSquareOneApiBaseUrl: modelConfig.baseUrl,
|
||||
};
|
||||
@@ -222,12 +231,16 @@ function importedUserModelMetadata(
|
||||
const metadata = { ...(existing?.metadata ?? {}) };
|
||||
delete metadata.worksSquareCredentialExpiresAt;
|
||||
delete metadata.worksSquareOneApiBaseUrl;
|
||||
delete metadata.worksSquareModelCapabilities;
|
||||
const credentialExpiresAt = modelConfig.apiKeyExpiresIn === null
|
||||
? undefined
|
||||
: new Date(nowMs + modelConfig.apiKeyExpiresIn * 1000).toISOString();
|
||||
return {
|
||||
...metadata,
|
||||
customModels: modelConfig.models,
|
||||
...(modelConfig.modelCapabilities
|
||||
? { worksSquareModelCapabilities: modelConfig.modelCapabilities }
|
||||
: {}),
|
||||
worksSquareCredentialMode: modelConfig.credentialMode,
|
||||
...(credentialExpiresAt ? { worksSquareCredentialExpiresAt: credentialExpiresAt } : {}),
|
||||
};
|
||||
@@ -295,6 +308,7 @@ function providerAccountRuntimeShape(account: ProviderAccount): unknown {
|
||||
isDefault: account.isDefault,
|
||||
metadata: {
|
||||
customModels: account.metadata?.customModels,
|
||||
worksSquareModelCapabilities: account.metadata?.worksSquareModelCapabilities,
|
||||
worksSquareCredentialMode: account.metadata?.worksSquareCredentialMode,
|
||||
worksSquareOneApiBaseUrl: account.metadata?.worksSquareOneApiBaseUrl,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user