feat: add docs

This commit is contained in:
LaZzyMan 2026-01-19 14:51:49 +08:00
parent a546e84887
commit 6e641b8def
14 changed files with 467 additions and 327 deletions

View file

@ -1,4 +1,11 @@
import type {
ExtensionConfig,
ExtensionRequestOptions,
SkillConfig,
SubagentConfig,
} from '@qwen-code/qwen-code-core';
import type { ConfirmationRequest } from '../../ui/types.js';
import chalk from 'chalk';
/**
* Requests consent from the user to perform an action, by reading a Y/n
@ -85,3 +92,106 @@ async function promptForConsentInteractive(
});
});
}
/**
* Builds a consent string for installing an extension based on it's
* extensionConfig.
*/
export function extensionConsentString(
extensionConfig: ExtensionConfig,
commands: string[] = [],
skills: SkillConfig[] = [],
subagents: SubagentConfig[] = [],
): string {
const output: string[] = [];
const mcpServerEntries = Object.entries(extensionConfig.mcpServers || {});
output.push(`Installing extension "${extensionConfig.name}".`);
output.push(
'**Extensions may introduce unexpected behavior. Ensure you have investigated the extension source and trust the author.**',
);
if (mcpServerEntries.length) {
output.push('This extension will run the following MCP servers:');
for (const [key, mcpServer] of mcpServerEntries) {
const isLocal = !!mcpServer.command;
const source =
mcpServer.httpUrl ??
`${mcpServer.command || ''}${mcpServer.args ? ' ' + mcpServer.args.join(' ') : ''}`;
output.push(` * ${key} (${isLocal ? 'local' : 'remote'}): ${source}`);
}
}
if (commands && commands.length > 0) {
output.push(
`This extension will add the following commands: ${commands.join(', ')}.`,
);
}
if (extensionConfig.contextFileName) {
output.push(
`This extension will append info to your QWEN.md context using ${extensionConfig.contextFileName}`,
);
}
if (extensionConfig.excludeTools) {
output.push(
`This extension will exclude the following core tools: ${extensionConfig.excludeTools}`,
);
}
if (skills.length > 0) {
output.push('This extension will install the following skills:');
for (const skill of skills) {
output.push(` * ${chalk.bold(skill.name)}: ${skill.description}`);
}
}
if (subagents.length > 0) {
output.push('This extension will install the following subagents:');
for (const subagent of subagents) {
output.push(` * ${chalk.bold(subagent.name)}: ${subagent.description}`);
}
}
return output.join('\n');
}
/**
* Requests consent from the user to install an extension (extensionConfig), if
* there is any difference between the consent string for `extensionConfig` and
* `previousExtensionConfig`.
*
* Always requests consent if previousExtensionConfig is null.
*
* Throws if the user does not consent.
*/
export const requestConsentOrFail = async (
requestConsent: (consent: string) => Promise<boolean>,
options?: ExtensionRequestOptions,
) => {
if (!options) return;
const {
extensionConfig,
commands = [],
skills = [],
subagents = [],
previousExtensionConfig,
previousCommands = [],
previousSkills = [],
previousSubagents = [],
} = options;
const extensionConsent = extensionConsentString(
extensionConfig,
commands,
skills,
subagents,
);
if (previousExtensionConfig) {
const previousExtensionConsent = extensionConsentString(
previousExtensionConfig,
previousCommands,
previousSkills,
previousSubagents,
);
if (previousExtensionConsent === extensionConsent) {
return;
}
}
if (!(await requestConsent(extensionConsent))) {
throw new Error(`Installation cancelled for "${extensionConfig.name}".`);
}
};

View file

@ -13,7 +13,10 @@ import {
import { getErrorMessage } from '../../utils/errors.js';
import { isWorkspaceTrusted } from '../../config/trustedFolders.js';
import { loadSettings } from '../../config/settings.js';
import { requestConsentNonInteractive } from './consent.js';
import {
requestConsentOrFail,
requestConsentNonInteractive,
} from './consent.js';
interface InstallArgs {
source: string;
@ -39,8 +42,8 @@ export async function handleInstall(args: InstallArgs) {
}
const requestConsent = args.consent
? () => Promise.resolve(true)
: requestConsentNonInteractive;
? () => Promise.resolve()
: requestConsentOrFail.bind(null, requestConsentNonInteractive);
const workspaceDir = process.cwd();
const extensionManager = new ExtensionManager({
workspaceDir,

View file

@ -7,7 +7,10 @@
import type { CommandModule } from 'yargs';
import { type ExtensionInstallMetadata } from '@qwen-code/qwen-code-core';
import { getErrorMessage } from '../../utils/errors.js';
import { requestConsentNonInteractive } from './consent.js';
import {
requestConsentNonInteractive,
requestConsentOrFail,
} from './consent.js';
import { getExtensionManager } from './utils.js';
interface InstallArgs {
@ -24,7 +27,7 @@ export async function handleLink(args: InstallArgs) {
const extension = await extensionManager.installExtension(
installMetadata,
requestConsentNonInteractive,
requestConsentOrFail.bind(null, requestConsentNonInteractive),
);
console.log(
`Extension "${extension.name}" linked successfully and enabled.`,

View file

@ -7,7 +7,10 @@
import type { CommandModule } from 'yargs';
import { getErrorMessage } from '../../utils/errors.js';
import { ExtensionManager } from '@qwen-code/qwen-code-core';
import { requestConsentNonInteractive } from './consent.js';
import {
requestConsentNonInteractive,
requestConsentOrFail,
} from './consent.js';
import { isWorkspaceTrusted } from '../../config/trustedFolders.js';
import { loadSettings } from '../../config/settings.js';
@ -20,7 +23,10 @@ export async function handleUninstall(args: UninstallArgs) {
const workspaceDir = process.cwd();
const extensionManager = new ExtensionManager({
workspaceDir,
requestConsent: requestConsentNonInteractive,
requestConsent: requestConsentOrFail.bind(
null,
requestConsentNonInteractive,
),
isWorkspaceTrusted: !!isWorkspaceTrusted(
loadSettings(workspaceDir).merged,
),

View file

@ -6,14 +6,20 @@
import { ExtensionManager } from '@qwen-code/qwen-code-core';
import { loadSettings } from '../../config/settings.js';
import { requestConsentNonInteractive } from './consent.js';
import {
requestConsentOrFail,
requestConsentNonInteractive,
} from './consent.js';
import { isWorkspaceTrusted } from '../../config/trustedFolders.js';
export async function getExtensionManager(): Promise<ExtensionManager> {
const workspaceDir = process.cwd();
const extensionManager = new ExtensionManager({
workspaceDir,
requestConsent: requestConsentNonInteractive,
requestConsent: requestConsentOrFail.bind(
null,
requestConsentNonInteractive,
),
isWorkspaceTrusted: !!isWorkspaceTrusted(loadSettings(workspaceDir).merged),
});
await extensionManager.refreshCache();

View file

@ -68,11 +68,6 @@ export function createSlashCommandFromDefinition(
.map((segment) => segment.replaceAll(':', '_'))
.join(':');
// Prefix command name with extension name if provided
const commandName = extensionName
? `${extensionName}:${baseCommandName}`
: baseCommandName;
// Add extension name tag for extension commands
const defaultDescription = `Custom command from ${path.basename(filePath)}`;
let description = definition.description || defaultDescription;
@ -109,7 +104,7 @@ export function createSlashCommandFromDefinition(
}
return {
name: commandName,
name: baseCommandName,
description,
kind: CommandKind.FILE,
extensionName,
@ -119,7 +114,7 @@ export function createSlashCommandFromDefinition(
): Promise<SlashCommandActionReturn> => {
if (!context.invocation) {
console.error(
`[FileCommandLoader] Critical error: Command '${commandName}' was executed without invocation context.`,
`[FileCommandLoader] Critical error: Command '${baseCommandName}' was executed without invocation context.`,
);
return {
type: 'submit_prompt',

View file

@ -102,7 +102,10 @@ import { processVisionSwitchOutcome } from './hooks/useVisionAutoSwitch.js';
import { useSubagentCreateDialog } from './hooks/useSubagentCreateDialog.js';
import { useAgentsManagerDialog } from './hooks/useAgentsManagerDialog.js';
import { useAttentionNotifications } from './hooks/useAttentionNotifications.js';
import { requestConsentInteractive } from '../commands/extensions/consent.js';
import {
requestConsentInteractive,
requestConsentOrFail,
} from '../commands/extensions/consent.js';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
@ -165,8 +168,10 @@ export const AppContainer = (props: AppContainerProps) => {
const extensionManager = config.getExtensionManager();
extensionManager.setRequestConsent(async (description) =>
requestConsentInteractive(description, addConfirmUpdateExtensionRequest),
extensionManager.setRequestConsent(
requestConsentOrFail.bind(null, (description) =>
requestConsentInteractive(description, addConfirmUpdateExtensionRequest),
),
);
const { addConfirmUpdateExtensionRequest, confirmUpdateExtensionRequests } =