qwen-code/packages/cli/src/services/BundledSkillLoader.ts
wenshao df3cfd1e83 fix(review): prepend YOUR_MODEL_ID declaration for model attribution
Some models (e.g., glm-5.1) ignore the {{model}} template in code
blocks and write their own footer without the model name. Fix:

1. BundledSkillLoader prepends YOUR_MODEL_ID="glm-5.1" as a top-level
   declaration at the start of the skill body — impossible to miss
2. SKILL.md references YOUR_MODEL_ID in footer instructions
3. Empty model → empty string (no "unknown" — prefer omission)
4. YOUR_MODEL_ID declaration only prepended when model is available

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:38:17 +08:00

90 lines
2.8 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config } from '@qwen-code/qwen-code-core';
import {
createDebugLogger,
appendToLastTextPart,
} from '@qwen-code/qwen-code-core';
import type { ICommandLoader } from './types.js';
import type {
SlashCommand,
SlashCommandActionReturn,
} from '../ui/commands/types.js';
import { CommandKind } from '../ui/commands/types.js';
const debugLogger = createDebugLogger('BUNDLED_SKILL_LOADER');
/**
* Loads bundled skills as slash commands, making them directly invocable
* via /<skill-name> (e.g., /review).
*/
export class BundledSkillLoader implements ICommandLoader {
constructor(private readonly config: Config | null) {}
async loadCommands(_signal: AbortSignal): Promise<SlashCommand[]> {
const skillManager = this.config?.getSkillManager();
if (!skillManager) {
debugLogger.debug('SkillManager not available, skipping bundled skills');
return [];
}
try {
const allSkills = await skillManager.listSkills({ level: 'bundled' });
// Hide skills whose allowedTools require cron when cron is disabled
const cronEnabled = this.config?.isCronEnabled() ?? false;
const skills = allSkills.filter((skill) => {
if (
!cronEnabled &&
skill.allowedTools?.some((t) => t.startsWith('cron_'))
) {
debugLogger.debug(
`Hiding skill "${skill.name}" because cron is not enabled`,
);
return false;
}
return true;
});
debugLogger.debug(
`Loaded ${skills.length} bundled skill(s) as slash commands`,
);
return skills.map((skill) => ({
name: skill.name,
description: skill.description,
kind: CommandKind.SKILL,
action: async (context, _args): Promise<SlashCommandActionReturn> => {
// Resolve template variables in skill body
let body = skill.body;
const modelId = this.config?.getModel()?.trim() || '';
if (body.includes('{{model}}') || body.includes('YOUR_MODEL_ID')) {
body = body.replaceAll('{{model}}', modelId);
body = body.replaceAll('YOUR_MODEL_ID', modelId);
// Prepend model identity as a top-level declaration so the LLM
// cannot miss it even if it doesn't copy the template exactly.
if (modelId) {
body = `YOUR_MODEL_ID="${modelId}"\n\n${body}`;
}
}
const content = context.invocation?.args
? appendToLastTextPart([{ text: body }], context.invocation.raw)
: [{ text: body }];
return {
type: 'submit_prompt',
content,
};
},
}));
} catch (error) {
debugLogger.error('Failed to load bundled skills:', error);
return [];
}
}
}