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:
inman
2026-09-03 10:44:04 +08:00
122 changed files with 10866 additions and 436 deletions

View File

@@ -71,11 +71,16 @@ import {
type CodingProjectPluginService,
type CodingPluginMarketplaceService,
} from './coding-product-services';
import { CODE_OWNED_OPTIONAL_BUNDLED_RELEASES } from '../../shared/coding-plugins';
import { ModelToolRegistry } from '../coding-runtime/pi/model-tools/model-tool-registry';
import { DevicePackageManager } from '../coding-packages/device-package-manager';
import { DevicePackageTools } from '../coding-packages/device-package-tools';
export interface CodingCompositionPaths {
executablePath: string;
cliPath: string;
serverPath: string;
npmCliPath?: string;
userDataDir: string;
bundledSkillsDir: string;
}
@@ -136,7 +141,7 @@ export function resolveCodingPiRuntimePaths(input: {
resourcesPath: string;
appPath: string;
executablePath: string;
}): Pick<CodingCompositionPaths, 'executablePath' | 'cliPath' | 'serverPath'> {
}): Pick<CodingCompositionPaths, 'executablePath' | 'cliPath' | 'serverPath' | 'npmCliPath'> {
return {
executablePath: resolvePiWorkerExecutablePath(input.executablePath),
cliPath: input.isPackaged
@@ -152,6 +157,9 @@ export function resolveCodingPiRuntimePaths(input: {
serverPath: input.isPackaged
? path.join(input.resourcesPath, 'resources', 'pi-agent-server.mjs')
: path.join(input.appPath, 'resources', 'pi-agent-server.mjs'),
npmCliPath: input.isPackaged
? path.join(input.resourcesPath, 'publish-runtime', 'bin', 'npm-cli.js')
: path.join(input.appPath, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
};
}
@@ -207,14 +215,26 @@ export function createCodingComposition(
};
};
let effectiveResolver: EffectivePluginResolver | undefined;
let invalidateDeviceResources = async (): Promise<void> => undefined;
const projectStore = options.projectStore ?? createCodingProjectStore(options.storage);
const attachments = new CodingAttachmentStore(
path.join(options.paths.userDataDir, 'coding-runtime', 'attachments'),
);
const modelToolRegistry = new ModelToolRegistry();
const devicePackageManager = new DevicePackageManager({
rootDir: path.join(options.paths.userDataDir, 'coding-runtime', 'device-packages'),
executablePath: options.paths.executablePath,
cliPath: options.paths.cliPath,
...(options.paths.npmCliPath ? { npmCliPath: options.paths.npmCliPath } : {}),
onGenerationChanged: async () => await invalidateDeviceResources(),
});
const devicePackageTools = new DevicePackageTools(devicePackageManager);
const productTools = new PiProductTools({
browser: options.browser,
attachments,
bundledSkillsDir: options.paths.bundledSkillsDir,
modelToolRegistry,
devicePackageTools,
pluginSkillSources,
getPluginSkillSources: async () => effectiveResolver
? (await effectiveResolver.getSkillSources()).map((source) => ({
@@ -287,6 +307,7 @@ export function createCodingComposition(
marketplace: marketplaceClient,
packageStore,
makeloreVersion: options.clientVersion ?? '2.0.0',
bundledReleases: CODE_OWNED_OPTIONAL_BUNDLED_RELEASES,
});
const policyClient = options.policyClient ?? new PluginPolicyClient();
const knownPluginIds = new Set(pluginDefinitions.map(({ id }) => id));
@@ -374,6 +395,9 @@ export function createCodingComposition(
: {}),
extensionHost,
capabilityRegistry,
modelToolRegistry,
devicePackageManager,
devicePackageTools: devicePackageTools.tools,
}),
});
const childOpener = createPiManagedSubagentChildOpener({
@@ -431,7 +455,7 @@ export function createCodingComposition(
},
});
const invalidateManagedResources = async (): Promise<void> => {
runtime?.markResourcesStale();
await runtime?.refreshResources();
const projectPaths = [...conversationStores.entries()];
for (const [, store] of projectPaths) {
const conversationsInProject = await store.read()
@@ -440,6 +464,7 @@ export function createCodingComposition(
for (const conversation of conversationsInProject) registry.forget(conversation.id);
}
};
invalidateDeviceResources = invalidateManagedResources;
const pluginMarketplace: CodingPluginMarketplaceService = createCodingPluginMarketplaceService({
marketplace: marketplaceClient,
packageStore,
@@ -471,6 +496,7 @@ export function createCodingComposition(
return {
attachments,
dataService,
devicePackages: devicePackageManager,
plugins,
pluginMarketplace,
marketplace: pluginMarketplace,
@@ -490,6 +516,7 @@ export function createCodingComposition(
await Promise.allSettled(conversationIds.map((conversationId) => (
runtime.dispose(conversationId, reason)
)));
if (reason === 'background_sleep' && runtime.hasActiveWork()) return;
await agentServer.stop();
},
async shutdown() {

View File

@@ -47,6 +47,7 @@ import type {
} from '../coding-plugins/package-store';
import type { MarketplaceLibrarySnapshot } from '../coding-plugins/account-plugin-cache';
import type { EffectivePluginResolver } from '../coding-plugins/effective-resolver';
import type { DevicePackageManager } from '../coding-packages/device-package-manager';
export interface ActiveCodingProject {
id: string;
@@ -66,6 +67,7 @@ export interface CodingProductHost {
export interface CodingProductComposition {
attachments: CodingAttachmentStore;
dataService: DataServiceOperations;
devicePackages: DevicePackageManager;
previewDataSession?: PreviewDataSessionManager;
productTools: PiProductTools;
pluginMarketplace: CodingPluginMarketplaceService;
@@ -674,7 +676,7 @@ export function createCodingProductHost(options: CodingProductHostOptions): Codi
async listSkills(agentId) {
const project = await activeProject();
const assignedSkillIds = await selectedSkillIds(project.path, agentId);
const effective = options.effectiveResolver
const effective = agentId && options.effectiveResolver
? await options.effectiveResolver.resolve({
projectId: project.id,
projectPath: project.path,
@@ -687,11 +689,17 @@ export function createCodingProductHost(options: CodingProductHostOptions): Codi
// still supplies the availability set; only the worker opener receives
// the effective subset.
const skillIds = assignedSkillIds;
const enabledPluginIds = effective
? effective.effectiveSkillIds
: options.getEnabledPluginIds
? await options.getEnabledPluginIds(project.path)
: [];
// The unscoped list is the Agent assignment catalog. It must expose
// every project-enabled plugin Skill before that Skill has been assigned;
// the resolver's effective subset is only authoritative once an Agent is
// selected (and for worker-facing projections).
const enabledPluginIds = !agentId && options.getEnabledPluginIds
? await options.getEnabledPluginIds(project.path)
: effective
? effective.effectiveSkillIds
: options.getEnabledPluginIds
? await options.getEnabledPluginIds(project.path)
: [];
return await options.productTools.listSkills(
skillIds,
enabledPluginIds,

View File

@@ -22,6 +22,7 @@ import { handleCodingProjectRoutes } from './routes/coding-projects';
import { handleCodingConversationRoutes } from './routes/coding-conversations';
import { handleCodingPluginRoutes } from './routes/coding-plugins';
import { handlePluginMarketplaceRoutes } from './routes/plugin-marketplace';
import { handleDevicePackageRoutes } from './routes/device-packages';
export type HostApiRouteHandler = (
req: IncomingMessage,
@@ -51,6 +52,7 @@ export const hostApiRouteHandlers: readonly HostApiRouteHandler[] = [
handleCodingAttachmentRoutes,
handleCodingProjectRoutes,
handlePluginMarketplaceRoutes,
handleDevicePackageRoutes,
handleCodingPluginRoutes,
handleCodingConversationRoutes,
handleCodingFileRoutes,

View File

@@ -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;

View 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;
}
}

View File

@@ -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,
},