Qwen Code 0.4.0 TypeScript SDK 完全实战指南


Qwen Code 0.4.0 TypeScript SDK 完全实战指南

从命令行工具到可编程 AI 平台的革命性升级 - 包含完整可运行的项目示例

前言

Qwen Code 0.4.0 发布了 TypeScript SDK,这是一个历史性的更新。它将 Qwen Code 从一个纯粹的命令行工具转变为一个可以嵌入到任何 TypeScript/JavaScript 应用中的可编程 AI 平台。

本文将提供完全可运行的实战指南,让你能够亲身体验这个 SDK 的强大能力。


1. 环境准备

1.1 系统要求

  • Node.js >= 20.0.0
  • npm 或 yarn
  • 基础的 TypeScript 知识(可选)

1.2 安装 Qwen Code CLI

# 全局安装 Qwen Code CLI
npm install -g @qwen-code/qwen-code@latest

# 验证安装
qwen --version
# 应该输出: 0.4.0

# 测试 CLI 是否工作(首次运行会要求认证)
qwen "Hello, test"

如果认证有问题,请参考官方文档设置认证(推荐使用 Qwen OAuth)。

1.3 创建项目目录

# 创建主项目目录
mkdir qwen-sdk-demo
cd qwen-sdk-demo

# 初始化 npm 项目
npm init -y

# 安装 SDK
npm install @qwen-code/sdk zod

# 安装 TypeScript 和开发工具
npm install --save-dev typescript @types/node tsx nodemon

# 创建配置文件
npx tsc --init

1.4 配置环境变量

创建 .env 文件:

# .env
QWEN_CODE_CLI_PATH=/home/你的用户名/.nvm/versions/node/v24.11.1/bin/qwen

重要: 将 /home/你的用户名/ 替换为你的实际路径。可以通过以下命令找到正确路径:

which qwen
# 输出类似: /home/yourname/.nvm/versions/node/v24.11.1/bin/qwen

1.5 配置 TypeScript

创建 tsconfig.json

{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

1.6 更新 package.json

{
"name": "qwen-sdk-demo",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js",
"test": "npm run build && npm run start"
},
"dependencies": {
"@qwen-code/sdk": "^0.4.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}

创建目录结构:

mkdir src
mkdir src/tools
mkdir src/agents
mkdir src/utils

2. 基础功能演示

2.1 创建基础配置

创建 src/utils/config.ts

// src/utils/config.ts
import dotenv from 'dotenv';

// 加载环境变量
dotenv.config();

export const config = {
qwenPath: process.env.QWEN_CODE_CLI_PATH || 'qwen',
projectPath: process.cwd(),
debug: process.env.NODE_ENV === 'development'
};

// 验证配置
if (!config.qwenPath || config.qwenPath === 'qwen') {
console.warn('⚠️ 警告: 未设置 QWEN_CODE_CLI_PATH,使用系统 PATH');
}

2.2 创建第一个 SDK 应用

创建 src/index.ts

// src/index.ts
import { query } from '@qwen-code/sdk';
import { config } from './utils/config';

async function basicDemo() {
console.log('🚀 Qwen Code SDK 基础演示\n');

try {
const result = query({
prompt: '你好!请用中文简单介绍一下你自己,限制在100字以内。',
options: {
pathToQwenExecutable: config.qwenPath,
cwd: config.projectPath,
debug: config.debug,
permissionMode: 'yolo'
}
});

console.log('🤖 AI 回复:');
for await (const message of result) {
if (message.type === 'assistant') {
console.log(message.message.content);
break;
}
}

console.log('\n✅ 基础演示完成!');
} catch (error) {
console.error('❌ 错误:', error.message);
if (error.message.includes('not found')) {
console.log('\n💡 解决建议:');
console.log('1. 检查 QWEN_CODE_CLI_PATH 环境变量是否正确设置');
console.log('2. 确保已安装 Qwen Code CLI: npm install -g @qwen-code/qwen-code');
console.log('3. 运行 which qwen 查找正确路径');
}
}
}

// 运行基础演示
basicDemo();

2.3 运行测试

# 确保在 qwen-sdk-demo 目录中
pwd

# 安装 dotenv 包(我们之前忘记安装了)
npm install dotenv

# 开发模式运行
npm run dev

如果一切正常,你应该看到类似输出:

🚀 Qwen Code SDK 基础演示

🤖 AI 回复:
你好!我是Qwen Code,专门处理软件工程任务的AI助手。我可以帮助你进行代码编写、调试、重构、文档生成等工作,同时也能够运行构建、测试等命令。我会严格遵守项目规范,确保代码质量和安全性。

✅ 基础演示完成!

3. 自定义工具开发

3.1 创建文件分析工具

创建 src/tools/fileTools.ts

// src/tools/fileTools.ts
import { tool } from '@qwen-code/sdk';
import { z } from 'zod';
import { readFileSync, readdirSync, statSync } from 'fs';
import { join, extname } from 'path';

// 文件分析工具
export const fileAnalyzerTool = tool(
'analyze_file',
'分析文件的详细信息,包括大小、行数、语言等',
{
filePath: z.string().describe('要分析的文件路径'),
includeContent: z.boolean().default(false).describe('是否包含文件内容预览')
},
async (args) => {
try {
const stats = statSync(args.filePath);
const content = readFileSync(args.filePath, 'utf-8');
const lines = content.split('\n').length;
const ext = extname(args.filePath);

// 简单的语言检测
let language = 'unknown';
const langMap: Record<string, string> = {
'.ts': 'TypeScript',
'.js': 'JavaScript',
'.py': 'Python',
'.java': 'Java',
'.cpp': 'C++',
'.c': 'C',
'.go': 'Go',
'.rs': 'Rust',
'.php': 'PHP',
'.rb': 'Ruby'
};
language = langMap[ext] || ext.substring(1).toUpperCase() || 'unknown';

const analysis = {
path: args.filePath,
size: `${(stats.size / 1024).toFixed(2)} KB`,
lines: lines,
language: language,
lastModified: stats.mtime.toISOString(),
contentPreview: args.includeContent ? content.substring(0, 200) + '...' : undefined
};

return {
content: [{
type: 'text',
text: JSON.stringify(analysis, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: `文件分析失败: ${error.message}`
}],
isError: true
};
}
}
);

// 目录结构分析工具
export const directoryAnalyzerTool = tool(
'analyze_directory',
'分析目录结构,统计文件类型和数量',
{
dirPath: z.string().describe('要分析的目录路径'),
depth: z.number().default(2).describe('分析深度'),
includeHidden: z.boolean().default(false).describe('是否包含隐藏文件')
},
async (args) => {
try {
const analyze = (dirPath: string, currentDepth: number): any => {
if (currentDepth <= 0) return null;

try {
const items = readdirSync(dirPath);
let fileCount = 0;
let dirCount = 0;
const fileTypes: Record<string, number> = {};
const structure: any[] = [];

for (const item of items) {
// 跳过隐藏文件(如果不包含)
if (!args.includeHidden && item.startsWith('.')) {
continue;
}

const fullPath = join(dirPath, item);
const stat = statSync(fullPath);

if (stat.isDirectory() && currentDepth > 1) {
dirCount++;
const subAnalysis = analyze(fullPath, currentDepth - 1);
structure.push({
name: item,
type: 'directory',
children: subAnalysis?.structure || []
});
} else if (stat.isFile()) {
fileCount++;
const ext = extname(item);
fileTypes[ext] = (fileTypes[ext] || 0) + 1;

structure.push({
name: item,
type: 'file',
size: `${(stat.size / 1024).toFixed(2)} KB`
});
}
}

return {
path: dirPath,
fileCount,
dirCount,
fileTypes,
structure: structure.slice(0, 10) // 限制显示数量
};
} catch (error) {
return { error: error.message };
}
};

const result = analyze(args.dirPath, args.depth);

return {
content: [{
type: 'text',
text: JSON.stringify(result, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: `目录分析失败: ${error.message}`
}],
isError: true
};
}
}
);

3.2 创建系统信息工具

创建 src/tools/systemTools.ts

// src/tools/systemTools.ts
import { tool } from '@qwen-code/sdk';
import { z } from 'zod';
import { execSync } from 'child_process';

// 系统信息工具
export const systemInfoTool = tool(
'get_system_info',
'获取系统环境信息',
{
detail: z.enum(['basic', 'full']).default('basic').describe('信息详细程度')
},
async (args) => {
try {
const os = await import('os');
const info: any = {
platform: os.platform(),
arch: os.arch(),
nodeVersion: process.version,
memory: {
total: `${Math.round(os.totalmem() / 1024 / 1024 / 1024)} GB`,
free: `${Math.round(os.freemem() / 1024 / 1024 / 1024)} GB`,
usage: `${Math.round(((os.totalmem() - os.freemem()) / os.totalmem()) * 100)}%`
},
cpus: os.cpus().length
};

if (args.detail === 'full') {
info.uptime = `${Math.round(os.uptime() / 3600)} hours`;
info.currentDirectory = process.cwd();
info.userInfo = os.userInfo();

try {
const gitVersion = execSync('git --version', { encoding: 'utf-8' }).trim();
info.gitVersion = gitVersion;
} catch {
info.gitVersion = 'Not installed';
}
}

return {
content: [{
type: 'text',
text: JSON.stringify(info, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: `系统信息获取失败: ${error.message}`
}],
isError: true
};
}
}
);

// 时间工具
export const timeTool = tool(
'get_time_info',
'获取当前时间和格式化信息',
{
timezone: z.string().default('local').describe('时区'),
format: z.enum(['iso', 'readable', 'timestamp']).default('readable').describe('时间格式')
},
async (args) => {
try {
const now = new Date();
let result: string;

switch (args.format) {
case 'iso':
result = now.toISOString();
break;
case 'timestamp':
result = now.getTime().toString();
break;
case 'readable':
result = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
weekday: 'long'
});
break;
}

const timeInfo = {
currentTime: result,
timezone: args.timezone,
unixTimestamp: now.getTime(),
dateString: now.toDateString(),
timeString: now.toTimeString()
};

return {
content: [{
type: 'text',
text: JSON.stringify(timeInfo, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: `时间信息获取失败: ${error.message}`
}],
isError: true
};
}
}
);

3.3 创建 MCP 服务器

创建 src/tools/index.ts

// src/tools/index.ts
import { createSdkMcpServer } from '@qwen-code/sdk';
import { fileAnalyzerTool, directoryAnalyzerTool } from './fileTools';
import { systemInfoTool, timeTool } from './systemTools';

// 创建自定义 MCP 服务器
export const demoServer = createSdkMcpServer({
name: 'demo-toolkit',
version: '1.0.0',
tools: [
fileAnalyzerTool,
directoryAnalyzerTool,
systemInfoTool,
timeTool
]
});

// 服务器信息
export const serverInfo = {
name: 'demo-toolkit',
tools: [
'analyze_file - 文件分析',
'analyze_directory - 目录分析',
'get_system_info - 系统信息',
'get_time_info - 时间信息'
]
};

4. 完整项目:智能项目分析器

4.1 创建主应用

创建 src/project-analyzer.ts

// src/project-analyzer.ts
import { query, createSdkMcpServer } from '@qwen-code/sdk';
import { config } from './utils/config';
import { demoServer, serverInfo } from './tools';

export class ProjectAnalyzer {
private mcpServer: any;

constructor() {
this.mcpServer = demoServer;
console.log('🔧 初始化项目分析器...');
console.log('📦 可用工具:', serverInfo.tools.join(', '));
}

async analyzeProject(projectPath: string = '.') {
console.log(`\n📊 开始分析项目: ${projectPath}\n`);

const prompt = `
请使用自定义工具对当前项目进行全面分析:

1. 首先获取系统信息
2. 分析当前目录结构(深度2级)
3. 如果发现 TypeScript/JavaScript 文件,选择1-2个重要文件进行分析
4. 获取当前时间信息
5. 最后生成一个完整的项目分析报告

请按以下格式输出报告:
## 项目分析报告
### 系统环境
### 项目结构
### 文件分析
### 总结和建议
`;

try {
const result = query({
prompt,
options: {
pathToQwenExecutable: config.qwenPath,
cwd: projectPath,
mcpServers: {
'demo-toolkit': this.mcpServer
},
permissionMode: 'yolo',
debug: config.debug,
maxSessionTurns: 10,
timeout: {
canUseTool: 30000,
mcpRequest: 60000,
controlRequest: 30000
}
}
});

console.log('🤖 AI 分析过程:');
let assistantResponse = '';

for await (const message of result) {
if (message.type === 'assistant') {
const content = message.message.content;
assistantResponse += content + '\n';

// 实时显示进度
if (content.includes('正在') || content.includes('分析') || content.includes('获取')) {
console.log('⏳', content);
}
} else if (message.type === 'result') {
console.log('\n✅ 分析完成!');
console.log('\n📋 最终报告:');
console.log(message.result);
}
}

return { success: true, report: assistantResponse };
} catch (error) {
console.error('❌ 分析失败:', error.message);
return { success: false, error: error.message };
}
}

async quickScan(filePath: string) {
console.log(`\n🔍 快速扫描文件: ${filePath}\n`);

const prompt = `
请快速分析指定的文件:${filePath}
使用文件分析工具获取详细信息,并给出简要评价。
`;

try {
const result = query({
prompt,
options: {
pathToQwenExecutable: config.qwenPath,
mcpServers: {
'demo-toolkit': this.mcpServer
},
permissionMode: 'yolo',
maxSessionTurns: 3
}
});

for await (const message of result) {
if (message.type === 'assistant') {
console.log('📄 文件分析结果:');
console.log(message.message.content);
break;
}
}
} catch (error) {
console.error('❌ 扫描失败:', error.message);
}
}
}

4.2 更新主入口文件

更新 src/index.ts

// src/index.ts
import { ProjectAnalyzer } from './project-analyzer';
import { config } from './utils/config';

async function main() {
console.log('🚀 Qwen Code 0.4.0 SDK 完整演示');
console.log('================================\n');

// 显示配置信息
console.log('⚙️ 配置信息:');
console.log(' Qwen CLI 路径:', config.qwenPath);
console.log(' 项目路径:', config.projectPath);
console.log(' 调试模式:', config.debug);
console.log('');

const analyzer = new ProjectAnalyzer();

// 基础测试
console.log('1️⃣ 基础功能测试');
console.log('-----------------');
await basicTest();

// 项目分析
console.log('\n2️⃣ 项目分析演示');
console.log('-----------------');
await analyzer.analyzeProject('./');

// 文件扫描演示
console.log('\n3️⃣ 文件扫描演示');
console.log('-----------------');
await analyzer.quickScan('./src/index.ts');

console.log('\n🎉 所有演示完成!');
console.log('\n💡 你已经成功体验了 Qwen Code SDK 的主要功能:');
console.log(' ✅ 基础查询和对话');
console.log(' ✅ 自定义工具开发');
console.log(' ✅ MCP 服务器集成');
console.log(' ✅ 完整项目构建');
console.log(' ✅ 错误处理和配置管理');
}

async function basicTest() {
try {
const result = query({
prompt: '请用一句话说明 Qwen Code SDK 的主要优势',
options: {
pathToQwenExecutable: config.qwenPath,
permissionMode: 'yolo',
maxSessionTurns: 1
}
});

for await (const message of result) {
if (message.type === 'assistant') {
console.log('🤖 AI 回复:', message.message.content);
break;
}
}
} catch (error) {
console.error('❌ 基础测试失败:', error.message);
}
}

// 运行主程序
main().catch(console.error);

4.3 运行完整演示

# 确保所有依赖都已安装
npm install

# 运行完整演示
npm run dev

你应该看到类似输出:

🚀 Qwen Code 0.4.0 SDK 完整演示
================================

⚙️ 配置信息:
Qwen CLI 路径: /home/yourname/.nvm/versions/node/v24.11.1/bin/qwen
项目路径: /path/to/qwen-sdk-demo
调试模式: true

🔧 初始化项目分析器...
📦 可用工具: analyze_file - 文件分析, analyze_directory - 目录分析, get_system_info - 系统信息, get_time_info - 时间信息

1️⃣ 基础功能测试
-----------------
🤖 AI 回复: Qwen Code SDK 提供完整的程序化访问能力,支持自定义工具开发、权限控制和多会话管理,让开发者能将AI能力深度集成到自己的应用中。

2️⃣ 项目分析演示
-----------------
📊 开始分析项目: ./

🤖 AI 分析过程:
⏳ 正在获取系统信息...
⏳ 开始分析项目目录结构...

✅ 分析完成!

📋 最终报告:
## 项目分析报告

### 系统环境
{
"platform": "linux",
"arch": "x64",
"nodeVersion": "v20.0.0",
"memory": {
"total": "16 GB",
"free": "8 GB",
"usage": "50%"
}
}

### 项目结构
{
"path": "./",
"fileCount": 8,
"dirCount": 3,
"fileTypes": {
".ts": 6,
".json": 2
}
}

### 文件分析
分析了 src/index.ts 和 tools/fileTools.ts...

### 总结和建议
这是一个标准的 TypeScript 项目,结构清晰,建议继续完善...

3️⃣ 文件扫描演示
-----------------
🔍 快速扫描文件: ./src/index.ts

📄 文件分析结果:
{
"path": "./src/index.ts",
"size": "2.45 KB",
"lines": 85,
"language": "TypeScript",
"lastModified": "2025-12-08T16:00:00.000Z"
}

🎉 所有演示完成!

💡 你已经成功体验了 Qwen Code SDK 的主要功能:
✅ 基础查询和对话
✅ 自定义工具开发
✅ MCP 服务器集成
✅ 完整项目构建
✅ 错误处理和配置管理

5. 进阶功能:权限控制

5.1 创建权限控制示例

创建 src/permission-demo.ts

// src/permission-demo.ts
import { query } from '@qwen-code/sdk';
import { config } from './utils/config';

async function permissionDemo() {
console.log('🔐 权限控制演示\n');

try {
const result = query({
prompt: '请尝试创建一个名为 permission-test.txt 的文件,写入"权限控制测试"内容',
options: {
pathToQwenExecutable: config.qwenPath,
cwd: config.projectPath,
permissionMode: 'default', // 默认需要权限确认
canUseTool: async (toolName, input, { signal }) => {
console.log(`🔍 权限请求: ${toolName}`);
console.log(`📝 参数:`, JSON.stringify(input, null, 2));

// 自定义权限逻辑
if (toolName === 'write_file') {
const filepath = input.filepath || input.path;

// 禁止写入系统目录
if (filepath.includes('/etc/') || filepath.includes('/usr/')) {
return {
behavior: 'deny',
message: '禁止写入系统目录'
};
}

// 检查文件扩展名
const allowedExtensions = ['.txt', '.md', '.json', '.ts', '.js'];
const ext = filepath.substring(filepath.lastIndexOf('.'));

if (!allowedExtensions.includes(ext)) {
return {
behavior: 'deny',
message: `不支持的文件类型: ${ext}`
};
}

console.log('✅ 权限已批准');
return { behavior: 'allow', updatedInput: input };
}

// 读取操作默认允许
if (toolName.startsWith('read_')) {
return { behavior: 'allow', updatedInput: input };
}

// 其他工具需要询问(实际应用中可以弹出确认对话框)
console.log('⚠️ 需要用户确认:', toolName);
return {
behavior: 'allow',
updatedInput: input // 演示中直接允许
};
}
}
});

for await (const message of result) {
if (message.type === 'assistant') {
console.log('🤖 AI 操作结果:');
console.log(message.message.content);
} else if (message.type === 'system' && message.data?.permission_denials) {
console.log('🚫 权限拒绝:', message.data.permission_denials);
}
}
} catch (error) {
console.error('❌ 权限演示失败:', error.message);
}
}

// 如果直接运行此文件
if (import.meta.url === `file://${process.argv[1]}`) {
permissionDemo();
}

5.2 运行权限控制演示

# 创建运行脚本
npx tsx src/permission-demo.ts

6. 项目部署和打包

6.1 构建项目

# 构建 TypeScript
npm run build

# 查看构建结果
ls -la dist/

6.2 创建可执行脚本

创建 scripts/run-analyzer.mjs

// scripts/run-analyzer.mjs
import { ProjectAnalyzer } from '../dist/project-analyzer.js';

const analyzer = new ProjectAnalyzer();

// 获取命令行参数
const projectPath = process.argv[2] || '.';

console.log('🔍 Qwen Code 项目分析器');
console.log('========================\n');

analyzer.analyzeProject(projectPath);

6.3 创建使用说明

创建 README.md

# Qwen Code SDK 演示项目

这是一个完整的 Qwen Code 0.4.0 TypeScript SDK 演示项目。

## 快速开始

1. 安装依赖:
```bash
npm install
  1. 配置环境变量:

    # 编辑 .env 文件
    nano .env
    # 添加你的 qwen CLI 路径
    QWEN_CODE_CLI_PATH=/path/to/your/qwen
  2. 运行演示:

    npm run dev

功能特性

  • ✅ 基础查询和对话
  • ✅ 自定义工具开发
  • ✅ MCP 服务器集成
  • ✅ 权限控制系统
  • ✅ 项目分析功能
  • ✅ 错误处理和配置管理

使用示例

基础查询

npx tsx src/index.ts

项目分析

npx tsx src/project-analyzer.ts

权限控制演示

npx tsx src/permission-demo.ts

项目结构

qwen-sdk-demo/
├── src/
│ ├── index.ts # 主入口
│ ├── project-analyzer.ts # 项目分析器
│ ├── permission-demo.ts # 权限控制演示
│ ├── tools/ # 自定义工具
│ │ ├── fileTools.ts # 文件相关工具
│ │ ├── systemTools.ts # 系统相关工具
│ │ └── index.ts # 工具导出
│ └── utils/
│ └── config.ts # 配置管理
├── scripts/ # 运行脚本
├── dist/ # 构建输出
└── package.json

---

## 7. 常见问题解决

### 7.1 路径问题

如果遇到 "qwen CLI not found" 错误:

```bash
# 查找 qwen 路径
which qwen

# 更新 .env 文件
echo "QWEN_CODE_CLI_PATH=$(which qwen)" >> .env

# 或者设置环境变量
export QWEN_CODE_CLI_PATH=$(which qwen)

7.2 权限问题

如果遇到权限错误:

# 确保 qwen 有执行权限
chmod +x $(which qwen)

# 如果需要 sudo 安装
sudo npm install -g @qwen-code/qwen-code

7.3 认证问题

如果遇到认证错误:

# 重新认证
qwen

# 或者使用 OpenAI 兼容模式
export OPENAI_API_KEY="your-api-key"
export OPENAI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"

7.4 调试模式

启用调试模式查看详细日志:

# 设置环境变量
export NODE_ENV=development

# 或者在代码中设置 debug: true

8. 总结

通过这个完整的实战项目,你已经体验了 Qwen Code 0.4.0 TypeScript SDK 的所有核心功能:

🎯 核心能力

  • 程序化访问: 通过 SDK 完全控制 Qwen Code
  • 自定义工具: 创建满足特定需求的 AI 工具
  • 权限控制: 精细控制 AI 的操作权限
  • 错误处理: 完善的错误处理和恢复机制

🚀 应用场景

  • IDE 插件: 集成到 VS Code、JetBrains 等
  • CI/CD 集成: 自动化代码审查和测试
  • 文档生成: 自动生成技术文档
  • 代码迁移: 智能代码重构和迁移

Qwen Code 0.4.0 SDK 不仅仅是一个工具,它是 AI 编程助手的新纪元。现在,你可以将 AI 能力嵌入到任何应用中,创造出前所未有的开发体验!


文章作者: huhuhuhr
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 huhuhuhr !
  目录