shellward 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/shellward)
|
|
10
10
|
[](./LICENSE)
|
|
11
|
-
[](#performance)
|
|
12
12
|
[](#performance)
|
|
13
13
|
|
|
14
14
|
[English](#demo) | [中文](#中文)
|
|
@@ -36,7 +36,7 @@ Outputs a red/yellow/green scorecard mapped to 网安法 / PIPL / 等保2.0 /
|
|
|
36
36
|
|
|
37
37
|
`npx shellward scan --json` for CI · `--ci` to fail the build on critical findings · see [GitHub Action](#github-action-pr-compliance-gate).
|
|
38
38
|
|
|
39
|
-
> Detects overseas-LLM endpoints (**data-export risk** — a China-only concept English tools ignore), hardcoded secrets, Chinese PII in files, and `.env` exposure.
|
|
39
|
+
> Detects overseas-LLM endpoints (**data-export risk** — a China-only concept English tools ignore), hardcoded secrets, Chinese PII in files, and `.env` exposure. When it finds an overseas model (e.g. an `openai` dependency), it **prescribes domestic compliant alternatives** (通义千问 / DeepSeek / Kimi / 智谱) with their OpenAI-compatible `base_url` — most migrations are just a `base_url` swap.
|
|
40
40
|
|
|
41
41
|
## Demo
|
|
42
42
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// 把 ComplianceReport 渲染成红黄绿评分卡(可截图传播 = 月1 获客钩子)。
|
|
4
4
|
// 按法规分组,每项给状态图标 + 结论 + 整改建议。
|
|
5
5
|
import { REGULATION_NAMES } from './regulations.js';
|
|
6
|
+
import { suggestDomestic } from '../rules/domestic-alternatives.js';
|
|
6
7
|
const STATUS_ICON = {
|
|
7
8
|
pass: '🟢',
|
|
8
9
|
warn: '🟡',
|
|
@@ -139,8 +140,55 @@ export function renderProjectFindings(scan, locale) {
|
|
|
139
140
|
}
|
|
140
141
|
L.push('');
|
|
141
142
|
}
|
|
143
|
+
// 处方:境内合规替代建议(仅当存在境外模型风险时)
|
|
144
|
+
L.push(...renderDomesticGuidance(scan, locale));
|
|
142
145
|
return L.join('\n');
|
|
143
146
|
}
|
|
147
|
+
/** 渲染「境内合规替代建议」—— 把数据出境风险变成可执行的迁移处方 */
|
|
148
|
+
function renderDomesticGuidance(scan, locale) {
|
|
149
|
+
const zh = locale === 'zh';
|
|
150
|
+
const overseas = scan.findings.filter(f => f.kind === 'overseas');
|
|
151
|
+
if (overseas.length === 0)
|
|
152
|
+
return [];
|
|
153
|
+
// 去重境外厂商
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
const providers = [];
|
|
156
|
+
for (const f of overseas) {
|
|
157
|
+
const key = (f.endpointId || f.provider_en || f.provider_zh || '').toLowerCase();
|
|
158
|
+
if (!key || seen.has(key))
|
|
159
|
+
continue;
|
|
160
|
+
seen.add(key);
|
|
161
|
+
providers.push({ key: f.endpointId || f.provider_en || '', zh: f.provider_zh, en: f.provider_en });
|
|
162
|
+
}
|
|
163
|
+
const L = [];
|
|
164
|
+
L.push(zh ? '## ✅ 境内合规替代建议' : '## ✅ Domestic Compliance Alternatives');
|
|
165
|
+
L.push('');
|
|
166
|
+
L.push(zh
|
|
167
|
+
? '把数据出境风险变成可执行的迁移路径。境内主流模型多为 **OpenAI 兼容**接口:'
|
|
168
|
+
: 'Turn data-export risk into a concrete migration path. Most domestic models are **OpenAI-compatible**:');
|
|
169
|
+
L.push('');
|
|
170
|
+
// 每个境外厂商的迁移难度
|
|
171
|
+
for (const p of providers) {
|
|
172
|
+
const s = suggestDomestic(p.key, p.zh, p.en);
|
|
173
|
+
L.push(zh
|
|
174
|
+
? `- **${s.overseas_zh}** → 迁移难度: ${s.difficulty_zh}`
|
|
175
|
+
: `- **${s.overseas_en}** → migration: ${s.difficulty_en}`);
|
|
176
|
+
}
|
|
177
|
+
L.push('');
|
|
178
|
+
// 共享的境内候选表(取第一个建议的 alternatives,避免重复)
|
|
179
|
+
const alts = suggestDomestic(providers[0].key, providers[0].zh, providers[0].en).alternatives;
|
|
180
|
+
L.push(zh ? '| 境内模型 | 厂商 | OpenAI 兼容 base_url |' : '| Domestic model | Vendor | OpenAI-compatible base_url |');
|
|
181
|
+
L.push('|---|---|---|');
|
|
182
|
+
for (const m of alts) {
|
|
183
|
+
L.push(`| ${zh ? m.name_zh : m.name_en} | ${m.vendor_zh} | \`${m.baseUrl}\` |`);
|
|
184
|
+
}
|
|
185
|
+
L.push('');
|
|
186
|
+
L.push(zh
|
|
187
|
+
? '> 对使用 `openai` SDK 的项目:通常仅需把 `base_url` 与 `api_key` 换成上表任一境内模型即可,业务代码无需改动。迁移前请以各厂商官方文档为准。'
|
|
188
|
+
: '> For projects using the `openai` SDK: usually just swap `base_url` + `api_key` to a domestic model above — no business-code change. Verify against each vendor’s official docs first.');
|
|
189
|
+
L.push('');
|
|
190
|
+
return L;
|
|
191
|
+
}
|
|
144
192
|
function scoreBar(score) {
|
|
145
193
|
const filled = Math.round(score / 5);
|
|
146
194
|
return '█'.repeat(filled) + '░'.repeat(20 - filled);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface DomesticModel {
|
|
2
|
+
id: string;
|
|
3
|
+
name_zh: string;
|
|
4
|
+
name_en: string;
|
|
5
|
+
vendor_zh: string;
|
|
6
|
+
/** OpenAI 兼容 base_url(若支持) */
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
/** 是否提供 OpenAI 兼容接口(决定迁移难度) */
|
|
9
|
+
openaiCompatible: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** 境内主流大模型(均为境内可备案/合规部署,按知名度排序) */
|
|
12
|
+
export declare const DOMESTIC_MODELS: DomesticModel[];
|
|
13
|
+
export interface DomesticSuggestion {
|
|
14
|
+
/** 触发的境外厂商(中文) */
|
|
15
|
+
overseas_zh: string;
|
|
16
|
+
overseas_en: string;
|
|
17
|
+
/** 迁移难度 */
|
|
18
|
+
difficulty_zh: string;
|
|
19
|
+
difficulty_en: string;
|
|
20
|
+
/** 推荐的境内替代(取兼容优先的前几个) */
|
|
21
|
+
alternatives: DomesticModel[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 针对某个境外厂商给出境内替代建议。
|
|
25
|
+
* @param key endpointId(如 'openai')或 provider_en(如 'OpenAI')
|
|
26
|
+
*/
|
|
27
|
+
export declare function suggestDomestic(key: string, provider_zh?: string, provider_en?: string): DomesticSuggestion;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/rules/domestic-alternatives.ts — 境内已备案大模型替代建议
|
|
2
|
+
//
|
|
3
|
+
// 扫到境外大模型(端点/SDK 依赖)后,给出可执行的「境内合规替代」处方:
|
|
4
|
+
// 把"你有数据出境风险"变成"换成这个、这样换"。这是 ShellWard 面向中国
|
|
5
|
+
// 市场最具差异化、最可执行的一环 —— 英文工具不会做。
|
|
6
|
+
//
|
|
7
|
+
// 杀手锏:境内主流模型多数提供 **OpenAI 兼容接口**,对 `openai` SDK 的项目
|
|
8
|
+
// 往往只需改 base_url + api key、代码零改动即可迁移到合规模型。
|
|
9
|
+
//
|
|
10
|
+
// 注:base_url 为各厂商公开的 OpenAI 兼容端点(可能随官方调整,迁移前以官方文档为准)。
|
|
11
|
+
/** 境内主流大模型(均为境内可备案/合规部署,按知名度排序) */
|
|
12
|
+
export const DOMESTIC_MODELS = [
|
|
13
|
+
{
|
|
14
|
+
id: 'qwen', name_zh: '通义千问', name_en: 'Qwen', vendor_zh: '阿里云百炼/DashScope',
|
|
15
|
+
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', openaiCompatible: true,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'deepseek', name_zh: 'DeepSeek', name_en: 'DeepSeek', vendor_zh: '深度求索',
|
|
19
|
+
baseUrl: 'https://api.deepseek.com', openaiCompatible: true,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'kimi', name_zh: 'Kimi', name_en: 'Kimi (Moonshot)', vendor_zh: '月之暗面',
|
|
23
|
+
baseUrl: 'https://api.moonshot.cn/v1', openaiCompatible: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'glm', name_zh: '智谱 GLM', name_en: 'Zhipu GLM', vendor_zh: '智谱 AI',
|
|
27
|
+
baseUrl: 'https://open.bigmodel.cn/api/paas/v4', openaiCompatible: true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'doubao', name_zh: '豆包', name_en: 'Doubao', vendor_zh: '字节火山方舟',
|
|
31
|
+
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', openaiCompatible: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'ernie', name_zh: '文心一言', name_en: 'ERNIE', vendor_zh: '百度千帆',
|
|
35
|
+
baseUrl: 'https://qianfan.baidubce.com/v2', openaiCompatible: true,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
// 哪些境外厂商走 OpenAI 兼容协议(其 SDK 项目可零代码迁移到境内兼容端点)
|
|
39
|
+
const OPENAI_PROTOCOL = new Set(['openai', 'azure-openai', 'groq', 'together', 'mistral', 'perplexity', 'openrouter', 'xai']);
|
|
40
|
+
/**
|
|
41
|
+
* 针对某个境外厂商给出境内替代建议。
|
|
42
|
+
* @param key endpointId(如 'openai')或 provider_en(如 'OpenAI')
|
|
43
|
+
*/
|
|
44
|
+
export function suggestDomestic(key, provider_zh, provider_en) {
|
|
45
|
+
const k = key.toLowerCase();
|
|
46
|
+
const isOpenAiProtocol = OPENAI_PROTOCOL.has(k) || /openai/.test(k);
|
|
47
|
+
// 推荐:OpenAI 兼容的境内模型优先(迁移最省事)
|
|
48
|
+
const alternatives = DOMESTIC_MODELS.filter(m => m.openaiCompatible).slice(0, 4);
|
|
49
|
+
const difficulty_zh = isOpenAiProtocol
|
|
50
|
+
? '低 — 多为 OpenAI 兼容协议,通常只需改 base_url + API key,代码零改动'
|
|
51
|
+
: '中 — SDK 不同,建议改用境内模型的 OpenAI 兼容端点并调整调用代码';
|
|
52
|
+
const difficulty_en = isOpenAiProtocol
|
|
53
|
+
? 'Low — OpenAI-compatible; usually just swap base_url + API key, no code change'
|
|
54
|
+
: 'Medium — different SDK; switch to a domestic OpenAI-compatible endpoint and adjust calls';
|
|
55
|
+
return {
|
|
56
|
+
overseas_zh: provider_zh || key,
|
|
57
|
+
overseas_en: provider_en || key,
|
|
58
|
+
difficulty_zh,
|
|
59
|
+
difficulty_en,
|
|
60
|
+
alternatives,
|
|
61
|
+
};
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shellward",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"mcpName": "io.github.jnMetaCode/shellward",
|
|
5
5
|
"description": "AI agent security & MCP security middleware — prompt injection detection, AI firewall, runtime guardrails & data-loss prevention for LLM tool calls. 8-layer defense against data exfiltration & dangerous commands. Zero dependencies. SDK + OpenClaw plugin. Supports LangChain, AutoGPT, Claude Code, Cursor, OpenAI Agents, Hermes Agent.",
|
|
6
6
|
"keywords": [
|
package/src/compliance/report.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { REGULATION_NAMES } from './regulations.js'
|
|
|
7
7
|
import type { Regulation } from './regulations.js'
|
|
8
8
|
import type { ComplianceReport, ControlResult, ControlStatus } from './audit.js'
|
|
9
9
|
import type { ProjectScanResult, FindingKind } from './project-scan.js'
|
|
10
|
+
import { suggestDomestic } from '../rules/domestic-alternatives.js'
|
|
10
11
|
|
|
11
12
|
const STATUS_ICON: Record<ControlStatus, string> = {
|
|
12
13
|
pass: '🟢',
|
|
@@ -157,9 +158,61 @@ export function renderProjectFindings(scan: ProjectScanResult, locale: 'zh' | 'e
|
|
|
157
158
|
}
|
|
158
159
|
L.push('')
|
|
159
160
|
}
|
|
161
|
+
|
|
162
|
+
// 处方:境内合规替代建议(仅当存在境外模型风险时)
|
|
163
|
+
L.push(...renderDomesticGuidance(scan, locale))
|
|
164
|
+
|
|
160
165
|
return L.join('\n')
|
|
161
166
|
}
|
|
162
167
|
|
|
168
|
+
/** 渲染「境内合规替代建议」—— 把数据出境风险变成可执行的迁移处方 */
|
|
169
|
+
function renderDomesticGuidance(scan: ProjectScanResult, locale: 'zh' | 'en'): string[] {
|
|
170
|
+
const zh = locale === 'zh'
|
|
171
|
+
const overseas = scan.findings.filter(f => f.kind === 'overseas')
|
|
172
|
+
if (overseas.length === 0) return []
|
|
173
|
+
|
|
174
|
+
// 去重境外厂商
|
|
175
|
+
const seen = new Set<string>()
|
|
176
|
+
const providers: { key: string; zh?: string; en?: string }[] = []
|
|
177
|
+
for (const f of overseas) {
|
|
178
|
+
const key = (f.endpointId || f.provider_en || f.provider_zh || '').toLowerCase()
|
|
179
|
+
if (!key || seen.has(key)) continue
|
|
180
|
+
seen.add(key)
|
|
181
|
+
providers.push({ key: f.endpointId || f.provider_en || '', zh: f.provider_zh, en: f.provider_en })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const L: string[] = []
|
|
185
|
+
L.push(zh ? '## ✅ 境内合规替代建议' : '## ✅ Domestic Compliance Alternatives')
|
|
186
|
+
L.push('')
|
|
187
|
+
L.push(zh
|
|
188
|
+
? '把数据出境风险变成可执行的迁移路径。境内主流模型多为 **OpenAI 兼容**接口:'
|
|
189
|
+
: 'Turn data-export risk into a concrete migration path. Most domestic models are **OpenAI-compatible**:')
|
|
190
|
+
L.push('')
|
|
191
|
+
|
|
192
|
+
// 每个境外厂商的迁移难度
|
|
193
|
+
for (const p of providers) {
|
|
194
|
+
const s = suggestDomestic(p.key, p.zh, p.en)
|
|
195
|
+
L.push(zh
|
|
196
|
+
? `- **${s.overseas_zh}** → 迁移难度: ${s.difficulty_zh}`
|
|
197
|
+
: `- **${s.overseas_en}** → migration: ${s.difficulty_en}`)
|
|
198
|
+
}
|
|
199
|
+
L.push('')
|
|
200
|
+
|
|
201
|
+
// 共享的境内候选表(取第一个建议的 alternatives,避免重复)
|
|
202
|
+
const alts = suggestDomestic(providers[0].key, providers[0].zh, providers[0].en).alternatives
|
|
203
|
+
L.push(zh ? '| 境内模型 | 厂商 | OpenAI 兼容 base_url |' : '| Domestic model | Vendor | OpenAI-compatible base_url |')
|
|
204
|
+
L.push('|---|---|---|')
|
|
205
|
+
for (const m of alts) {
|
|
206
|
+
L.push(`| ${zh ? m.name_zh : m.name_en} | ${m.vendor_zh} | \`${m.baseUrl}\` |`)
|
|
207
|
+
}
|
|
208
|
+
L.push('')
|
|
209
|
+
L.push(zh
|
|
210
|
+
? '> 对使用 `openai` SDK 的项目:通常仅需把 `base_url` 与 `api_key` 换成上表任一境内模型即可,业务代码无需改动。迁移前请以各厂商官方文档为准。'
|
|
211
|
+
: '> For projects using the `openai` SDK: usually just swap `base_url` + `api_key` to a domestic model above — no business-code change. Verify against each vendor’s official docs first.')
|
|
212
|
+
L.push('')
|
|
213
|
+
return L
|
|
214
|
+
}
|
|
215
|
+
|
|
163
216
|
function scoreBar(score: number): string {
|
|
164
217
|
const filled = Math.round(score / 5)
|
|
165
218
|
return '█'.repeat(filled) + '░'.repeat(20 - filled)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/rules/domestic-alternatives.ts — 境内已备案大模型替代建议
|
|
2
|
+
//
|
|
3
|
+
// 扫到境外大模型(端点/SDK 依赖)后,给出可执行的「境内合规替代」处方:
|
|
4
|
+
// 把"你有数据出境风险"变成"换成这个、这样换"。这是 ShellWard 面向中国
|
|
5
|
+
// 市场最具差异化、最可执行的一环 —— 英文工具不会做。
|
|
6
|
+
//
|
|
7
|
+
// 杀手锏:境内主流模型多数提供 **OpenAI 兼容接口**,对 `openai` SDK 的项目
|
|
8
|
+
// 往往只需改 base_url + api key、代码零改动即可迁移到合规模型。
|
|
9
|
+
//
|
|
10
|
+
// 注:base_url 为各厂商公开的 OpenAI 兼容端点(可能随官方调整,迁移前以官方文档为准)。
|
|
11
|
+
|
|
12
|
+
export interface DomesticModel {
|
|
13
|
+
id: string
|
|
14
|
+
name_zh: string
|
|
15
|
+
name_en: string
|
|
16
|
+
vendor_zh: string
|
|
17
|
+
/** OpenAI 兼容 base_url(若支持) */
|
|
18
|
+
baseUrl: string
|
|
19
|
+
/** 是否提供 OpenAI 兼容接口(决定迁移难度) */
|
|
20
|
+
openaiCompatible: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 境内主流大模型(均为境内可备案/合规部署,按知名度排序) */
|
|
24
|
+
export const DOMESTIC_MODELS: DomesticModel[] = [
|
|
25
|
+
{
|
|
26
|
+
id: 'qwen', name_zh: '通义千问', name_en: 'Qwen', vendor_zh: '阿里云百炼/DashScope',
|
|
27
|
+
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', openaiCompatible: true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'deepseek', name_zh: 'DeepSeek', name_en: 'DeepSeek', vendor_zh: '深度求索',
|
|
31
|
+
baseUrl: 'https://api.deepseek.com', openaiCompatible: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'kimi', name_zh: 'Kimi', name_en: 'Kimi (Moonshot)', vendor_zh: '月之暗面',
|
|
35
|
+
baseUrl: 'https://api.moonshot.cn/v1', openaiCompatible: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'glm', name_zh: '智谱 GLM', name_en: 'Zhipu GLM', vendor_zh: '智谱 AI',
|
|
39
|
+
baseUrl: 'https://open.bigmodel.cn/api/paas/v4', openaiCompatible: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'doubao', name_zh: '豆包', name_en: 'Doubao', vendor_zh: '字节火山方舟',
|
|
43
|
+
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', openaiCompatible: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'ernie', name_zh: '文心一言', name_en: 'ERNIE', vendor_zh: '百度千帆',
|
|
47
|
+
baseUrl: 'https://qianfan.baidubce.com/v2', openaiCompatible: true,
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
export interface DomesticSuggestion {
|
|
52
|
+
/** 触发的境外厂商(中文) */
|
|
53
|
+
overseas_zh: string
|
|
54
|
+
overseas_en: string
|
|
55
|
+
/** 迁移难度 */
|
|
56
|
+
difficulty_zh: string
|
|
57
|
+
difficulty_en: string
|
|
58
|
+
/** 推荐的境内替代(取兼容优先的前几个) */
|
|
59
|
+
alternatives: DomesticModel[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 哪些境外厂商走 OpenAI 兼容协议(其 SDK 项目可零代码迁移到境内兼容端点)
|
|
63
|
+
const OPENAI_PROTOCOL = new Set(['openai', 'azure-openai', 'groq', 'together', 'mistral', 'perplexity', 'openrouter', 'xai'])
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 针对某个境外厂商给出境内替代建议。
|
|
67
|
+
* @param key endpointId(如 'openai')或 provider_en(如 'OpenAI')
|
|
68
|
+
*/
|
|
69
|
+
export function suggestDomestic(key: string, provider_zh?: string, provider_en?: string): DomesticSuggestion {
|
|
70
|
+
const k = key.toLowerCase()
|
|
71
|
+
const isOpenAiProtocol = OPENAI_PROTOCOL.has(k) || /openai/.test(k)
|
|
72
|
+
|
|
73
|
+
// 推荐:OpenAI 兼容的境内模型优先(迁移最省事)
|
|
74
|
+
const alternatives = DOMESTIC_MODELS.filter(m => m.openaiCompatible).slice(0, 4)
|
|
75
|
+
|
|
76
|
+
const difficulty_zh = isOpenAiProtocol
|
|
77
|
+
? '低 — 多为 OpenAI 兼容协议,通常只需改 base_url + API key,代码零改动'
|
|
78
|
+
: '中 — SDK 不同,建议改用境内模型的 OpenAI 兼容端点并调整调用代码'
|
|
79
|
+
const difficulty_en = isOpenAiProtocol
|
|
80
|
+
? 'Low — OpenAI-compatible; usually just swap base_url + API key, no code change'
|
|
81
|
+
: 'Medium — different SDK; switch to a domestic OpenAI-compatible endpoint and adjust calls'
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
overseas_zh: provider_zh || key,
|
|
85
|
+
overseas_en: provider_en || key,
|
|
86
|
+
difficulty_zh,
|
|
87
|
+
difficulty_en,
|
|
88
|
+
alternatives,
|
|
89
|
+
}
|
|
90
|
+
}
|