mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-08 18:09:13 +00:00
Compare commits
5 Commits
fix/missin
...
v0.6.0-pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53604963e8 | ||
|
|
105ad743fa | ||
|
|
ac3f7cb8c8 | ||
|
|
7b01b26ff5 | ||
|
|
0f3e97ea1c |
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0-preview.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0-preview.2",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
@@ -17316,7 +17316,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0-preview.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "1.30.0",
|
"@google/genai": "1.30.0",
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
@@ -17953,7 +17953,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@qwen-code/qwen-code-core",
|
"name": "@qwen-code/qwen-code-core",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0-preview.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.36.1",
|
"@anthropic-ai/sdk": "^0.36.1",
|
||||||
@@ -21413,7 +21413,7 @@
|
|||||||
},
|
},
|
||||||
"packages/test-utils": {
|
"packages/test-utils": {
|
||||||
"name": "@qwen-code/qwen-code-test-utils",
|
"name": "@qwen-code/qwen-code-test-utils",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0-preview.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -21425,7 +21425,7 @@
|
|||||||
},
|
},
|
||||||
"packages/vscode-ide-companion": {
|
"packages/vscode-ide-companion": {
|
||||||
"name": "qwen-code-vscode-ide-companion",
|
"name": "qwen-code-vscode-ide-companion",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0-preview.2",
|
||||||
"license": "LICENSE",
|
"license": "LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0-preview.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"url": "git+https://github.com/QwenLM/qwen-code.git"
|
"url": "git+https://github.com/QwenLM/qwen-code.git"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0"
|
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0-preview.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env node scripts/start.js",
|
"start": "cross-env node scripts/start.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0-preview.2",
|
||||||
"description": "Qwen Code",
|
"description": "Qwen Code",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0"
|
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0-preview.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "1.30.0",
|
"@google/genai": "1.30.0",
|
||||||
|
|||||||
@@ -630,67 +630,6 @@ describe('BaseJsonOutputAdapter', () => {
|
|||||||
|
|
||||||
expect(state.blocks).toHaveLength(0);
|
expect(state.blocks).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should preserve whitespace in thinking content', () => {
|
|
||||||
const state = adapter.exposeCreateMessageState();
|
|
||||||
adapter.startAssistantMessage();
|
|
||||||
|
|
||||||
adapter.exposeAppendThinking(
|
|
||||||
state,
|
|
||||||
'',
|
|
||||||
'The user just said "Hello"',
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(state.blocks).toHaveLength(1);
|
|
||||||
expect(state.blocks[0]).toMatchObject({
|
|
||||||
type: 'thinking',
|
|
||||||
thinking: 'The user just said "Hello"',
|
|
||||||
});
|
|
||||||
// Verify spaces are preserved
|
|
||||||
const block = state.blocks[0] as { thinking: string };
|
|
||||||
expect(block.thinking).toContain('user just');
|
|
||||||
expect(block.thinking).not.toContain('userjust');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should preserve whitespace when appending multiple thinking fragments', () => {
|
|
||||||
const state = adapter.exposeCreateMessageState();
|
|
||||||
adapter.startAssistantMessage();
|
|
||||||
|
|
||||||
// Simulate streaming thinking content in fragments
|
|
||||||
adapter.exposeAppendThinking(state, '', 'The user just', null);
|
|
||||||
adapter.exposeAppendThinking(state, '', ' said "Hello"', null);
|
|
||||||
adapter.exposeAppendThinking(
|
|
||||||
state,
|
|
||||||
'',
|
|
||||||
'. This is a simple greeting',
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(state.blocks).toHaveLength(1);
|
|
||||||
const block = state.blocks[0] as { thinking: string };
|
|
||||||
// Verify the complete text with all spaces preserved
|
|
||||||
expect(block.thinking).toBe(
|
|
||||||
'The user just said "Hello". This is a simple greeting',
|
|
||||||
);
|
|
||||||
// Verify specific space preservation
|
|
||||||
expect(block.thinking).toContain('user just ');
|
|
||||||
expect(block.thinking).toContain(' said');
|
|
||||||
expect(block.thinking).toContain('". This');
|
|
||||||
expect(block.thinking).not.toContain('userjust');
|
|
||||||
expect(block.thinking).not.toContain('justsaid');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should preserve leading and trailing whitespace in description', () => {
|
|
||||||
const state = adapter.exposeCreateMessageState();
|
|
||||||
adapter.startAssistantMessage();
|
|
||||||
|
|
||||||
adapter.exposeAppendThinking(state, '', ' content with spaces ', null);
|
|
||||||
|
|
||||||
expect(state.blocks).toHaveLength(1);
|
|
||||||
const block = state.blocks[0] as { thinking: string };
|
|
||||||
expect(block.thinking).toBe(' content with spaces ');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('appendToolUse', () => {
|
describe('appendToolUse', () => {
|
||||||
|
|||||||
@@ -816,18 +816,9 @@ export abstract class BaseJsonOutputAdapter {
|
|||||||
parentToolUseId?: string | null,
|
parentToolUseId?: string | null,
|
||||||
): void {
|
): void {
|
||||||
const actualParentToolUseId = parentToolUseId ?? null;
|
const actualParentToolUseId = parentToolUseId ?? null;
|
||||||
|
const fragment = [subject?.trim(), description?.trim()]
|
||||||
// Build fragment without trimming to preserve whitespace in streaming content
|
.filter((value) => value && value.length > 0)
|
||||||
// Only filter out null/undefined/empty values
|
.join(': ');
|
||||||
const parts: string[] = [];
|
|
||||||
if (subject && subject.length > 0) {
|
|
||||||
parts.push(subject);
|
|
||||||
}
|
|
||||||
if (description && description.length > 0) {
|
|
||||||
parts.push(description);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fragment = parts.join(': ');
|
|
||||||
if (!fragment) {
|
if (!fragment) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,68 +323,6 @@ describe('StreamJsonOutputAdapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should preserve whitespace in thinking content (issue #1356)', () => {
|
|
||||||
adapter.processEvent({
|
|
||||||
type: GeminiEventType.Thought,
|
|
||||||
value: {
|
|
||||||
subject: '',
|
|
||||||
description: 'The user just said "Hello"',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const message = adapter.finalizeAssistantMessage();
|
|
||||||
expect(message.message.content).toHaveLength(1);
|
|
||||||
const block = message.message.content[0] as {
|
|
||||||
type: string;
|
|
||||||
thinking: string;
|
|
||||||
};
|
|
||||||
expect(block.type).toBe('thinking');
|
|
||||||
expect(block.thinking).toBe('The user just said "Hello"');
|
|
||||||
// Verify spaces are preserved
|
|
||||||
expect(block.thinking).toContain('user just');
|
|
||||||
expect(block.thinking).not.toContain('userjust');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should preserve whitespace when streaming multiple thinking fragments (issue #1356)', () => {
|
|
||||||
// Simulate streaming thinking content in multiple events
|
|
||||||
adapter.processEvent({
|
|
||||||
type: GeminiEventType.Thought,
|
|
||||||
value: {
|
|
||||||
subject: '',
|
|
||||||
description: 'The user just',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
adapter.processEvent({
|
|
||||||
type: GeminiEventType.Thought,
|
|
||||||
value: {
|
|
||||||
subject: '',
|
|
||||||
description: ' said "Hello"',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
adapter.processEvent({
|
|
||||||
type: GeminiEventType.Thought,
|
|
||||||
value: {
|
|
||||||
subject: '',
|
|
||||||
description: '. This is a simple greeting',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const message = adapter.finalizeAssistantMessage();
|
|
||||||
expect(message.message.content).toHaveLength(1);
|
|
||||||
const block = message.message.content[0] as {
|
|
||||||
type: string;
|
|
||||||
thinking: string;
|
|
||||||
};
|
|
||||||
expect(block.thinking).toBe(
|
|
||||||
'The user just said "Hello". This is a simple greeting',
|
|
||||||
);
|
|
||||||
// Verify specific spaces are preserved
|
|
||||||
expect(block.thinking).toContain('user just ');
|
|
||||||
expect(block.thinking).toContain(' said');
|
|
||||||
expect(block.thinking).not.toContain('userjust');
|
|
||||||
expect(block.thinking).not.toContain('justsaid');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should append tool use from ToolCallRequest events', () => {
|
it('should append tool use from ToolCallRequest events', () => {
|
||||||
adapter.processEvent({
|
adapter.processEvent({
|
||||||
type: GeminiEventType.ToolCallRequest,
|
type: GeminiEventType.ToolCallRequest,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@qwen-code/qwen-code-core",
|
"name": "@qwen-code/qwen-code-core",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0-preview.2",
|
||||||
"description": "Qwen Code Core",
|
"description": "Qwen Code Core",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -169,6 +169,44 @@ describe('ShellTool', () => {
|
|||||||
});
|
});
|
||||||
expect(invocation.getDescription()).not.toContain('[background]');
|
expect(invocation.getDescription()).not.toContain('[background]');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('is_background parameter coercion', () => {
|
||||||
|
it('should accept string "true" as boolean true', () => {
|
||||||
|
const invocation = shellTool.build({
|
||||||
|
command: 'npm run dev',
|
||||||
|
is_background: 'true' as unknown as boolean,
|
||||||
|
});
|
||||||
|
expect(invocation).toBeDefined();
|
||||||
|
expect(invocation.getDescription()).toContain('[background]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept string "false" as boolean false', () => {
|
||||||
|
const invocation = shellTool.build({
|
||||||
|
command: 'npm run build',
|
||||||
|
is_background: 'false' as unknown as boolean,
|
||||||
|
});
|
||||||
|
expect(invocation).toBeDefined();
|
||||||
|
expect(invocation.getDescription()).not.toContain('[background]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept string "True" as boolean true', () => {
|
||||||
|
const invocation = shellTool.build({
|
||||||
|
command: 'npm run dev',
|
||||||
|
is_background: 'True' as unknown as boolean,
|
||||||
|
});
|
||||||
|
expect(invocation).toBeDefined();
|
||||||
|
expect(invocation.getDescription()).toContain('[background]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept string "False" as boolean false', () => {
|
||||||
|
const invocation = shellTool.build({
|
||||||
|
command: 'npm run build',
|
||||||
|
is_background: 'False' as unknown as boolean,
|
||||||
|
});
|
||||||
|
expect(invocation).toBeDefined();
|
||||||
|
expect(invocation.getDescription()).not.toContain('[background]');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('execute', () => {
|
describe('execute', () => {
|
||||||
|
|||||||
@@ -122,4 +122,91 @@ describe('SchemaValidator', () => {
|
|||||||
};
|
};
|
||||||
expect(SchemaValidator.validate(schema, params)).not.toBeNull();
|
expect(SchemaValidator.validate(schema, params)).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('boolean string coercion', () => {
|
||||||
|
const booleanSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
is_background: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['is_background'],
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should coerce string "true" to boolean true', () => {
|
||||||
|
const params = { is_background: 'true' };
|
||||||
|
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||||
|
expect(params.is_background).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should coerce string "True" to boolean true', () => {
|
||||||
|
const params = { is_background: 'True' };
|
||||||
|
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||||
|
expect(params.is_background).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should coerce string "TRUE" to boolean true', () => {
|
||||||
|
const params = { is_background: 'TRUE' };
|
||||||
|
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||||
|
expect(params.is_background).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should coerce string "false" to boolean false', () => {
|
||||||
|
const params = { is_background: 'false' };
|
||||||
|
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||||
|
expect(params.is_background).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should coerce string "False" to boolean false', () => {
|
||||||
|
const params = { is_background: 'False' };
|
||||||
|
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||||
|
expect(params.is_background).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should coerce string "FALSE" to boolean false', () => {
|
||||||
|
const params = { is_background: 'FALSE' };
|
||||||
|
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||||
|
expect(params.is_background).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested objects with string booleans', () => {
|
||||||
|
const nestedSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
options: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
enabled: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const params = { options: { enabled: 'true' } };
|
||||||
|
expect(SchemaValidator.validate(nestedSchema, params)).toBeNull();
|
||||||
|
expect((params.options as unknown as { enabled: boolean }).enabled).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not affect non-boolean strings', () => {
|
||||||
|
const mixedSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' },
|
||||||
|
is_active: { type: 'boolean' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const params = { name: 'trueman', is_active: 'true' };
|
||||||
|
expect(SchemaValidator.validate(mixedSchema, params)).toBeNull();
|
||||||
|
expect(params.name).toBe('trueman');
|
||||||
|
expect(params.is_active).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass through actual boolean values unchanged', () => {
|
||||||
|
const params = { is_background: true };
|
||||||
|
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||||
|
expect(params.is_background).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,14 +41,12 @@ export class SchemaValidator {
|
|||||||
return 'Value of params must be an object';
|
return 'Value of params must be an object';
|
||||||
}
|
}
|
||||||
const validate = ajValidator.compile(schema);
|
const validate = ajValidator.compile(schema);
|
||||||
const valid = validate(data);
|
let valid = validate(data);
|
||||||
if (!valid && validate.errors) {
|
if (!valid && validate.errors) {
|
||||||
// Find any True or False values and lowercase them
|
// Coerce string boolean values ("true"/"false") to actual booleans
|
||||||
fixBooleanCasing(data as Record<string, unknown>);
|
fixBooleanValues(data as Record<string, unknown>);
|
||||||
|
|
||||||
const validate = ajValidator.compile(schema);
|
|
||||||
const valid = validate(data);
|
|
||||||
|
|
||||||
|
valid = validate(data);
|
||||||
if (!valid && validate.errors) {
|
if (!valid && validate.errors) {
|
||||||
return ajValidator.errorsText(validate.errors, { dataVar: 'params' });
|
return ajValidator.errorsText(validate.errors, { dataVar: 'params' });
|
||||||
}
|
}
|
||||||
@@ -57,13 +55,29 @@ export class SchemaValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixBooleanCasing(data: Record<string, unknown>) {
|
/**
|
||||||
|
* Coerces string boolean values to actual booleans.
|
||||||
|
* This handles cases where LLMs return "true"/"false" strings instead of boolean values,
|
||||||
|
* which is common with self-hosted LLMs.
|
||||||
|
*
|
||||||
|
* Converts:
|
||||||
|
* - "true", "True", "TRUE" -> true
|
||||||
|
* - "false", "False", "FALSE" -> false
|
||||||
|
*/
|
||||||
|
function fixBooleanValues(data: Record<string, unknown>) {
|
||||||
for (const key of Object.keys(data)) {
|
for (const key of Object.keys(data)) {
|
||||||
if (!(key in data)) continue;
|
if (!(key in data)) continue;
|
||||||
|
const value = data[key];
|
||||||
|
|
||||||
if (typeof data[key] === 'object') {
|
if (typeof value === 'object' && value !== null) {
|
||||||
fixBooleanCasing(data[key] as Record<string, unknown>);
|
fixBooleanValues(value as Record<string, unknown>);
|
||||||
} else if (data[key] === 'True') data[key] = 'true';
|
} else if (typeof value === 'string') {
|
||||||
else if (data[key] === 'False') data[key] = 'false';
|
const lower = value.toLowerCase();
|
||||||
|
if (lower === 'true') {
|
||||||
|
data[key] = true;
|
||||||
|
} else if (lower === 'false') {
|
||||||
|
data[key] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@qwen-code/qwen-code-test-utils",
|
"name": "@qwen-code/qwen-code-test-utils",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0-preview.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "qwen-code-vscode-ide-companion",
|
"name": "qwen-code-vscode-ide-companion",
|
||||||
"displayName": "Qwen Code Companion",
|
"displayName": "Qwen Code Companion",
|
||||||
"description": "Enable Qwen Code with direct access to your VS Code workspace.",
|
"description": "Enable Qwen Code with direct access to your VS Code workspace.",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0-preview.2",
|
||||||
"publisher": "qwenlm",
|
"publisher": "qwenlm",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
Reference in New Issue
Block a user