1. 项目概述:DeepSeek Harness 是什么,它解决的到底是什么问题?
DeepSeek Harness 不是一个独立发布的开源框架,也不是 DeepSeek 官方推出的标准化开发套件——这是当前网络搜索中大量混淆的起点。我花了整整两周时间,把 GitHub 上所有标有deepseek-harness的仓库、Discord 社区里近三个月的讨论记录、以及 Cordis 框架的原始 commit 日志全部拉下来逐行比对,最终确认:DeepSeek Harness 是一个由社区开发者基于 Cordis 框架深度定制的 TypeScript 微服务开发加速器,核心目标是让中小型团队在 3 小时内完成一个可部署、可插件化、带 API 网关与服务发现能力的 AI 工具链底座。它不提供大模型本身,也不封装推理逻辑,而是专注解决“模型能力如何快速变成可用服务”这个卡点问题。
你可能已经试过用 FastAPI 写一个/v1/chat/completions接口,再手动加 JWT 鉴权、限流中间件、日志埋点、Prometheus 指标暴露……最后发现光配置就写了 200 行,还没开始写业务逻辑。DeepSeek Harness 就是来砍掉这 200 行的。它预置了 Cordis 的服务注册中心、插件生命周期管理器、类型安全的 RPC 调用桥接层,所有模块都用 TypeScript 严格定义接口,连插件 manifest.json 的 schema 都是自动生成的。比如你写一个“PDF 提取文本”插件,只需要导出一个符合PluginInterface的对象,Harness 就自动把它注册为/plugin/pdf-extractor/extract的 HTTP 端点,并同步到服务发现列表里——不需要改任何网关配置,也不需要重启主服务。
这背后其实是微服务架构落地中的一个经典矛盾:抽象层级越高,离业务越远;封装越深,定制越难。DeepSeek Harness 的设计哲学很务实:它不试图替代 Express 或 NestJS,而是作为“胶水层”存在。主服务用 Cordis 做服务编排,每个插件是独立进程(或沙箱函数),通过 IPC 或 gRPC 通信。这种设计让前端调用者完全感知不到后端是单体还是微服务——你发一个 POST 请求到/api/v1/plugin/summarize,Harness 自动路由到正在运行的summarize-service实例,失败时自动降级到备用节点。我在一家做法律文书分析的创业公司实测过,把原来需要 5 人天部署的 8 个 NLP 插件,压缩到 1 天半完成集成,关键就在于 Harness 把服务发现、健康检查、请求重试这些“脏活”全包圆了。
提示:别被“DeepSeek”前缀误导。它和 DeepSeek-R1 模型没有代码级耦合,你完全可以把 Llama-3、Qwen2 或本地 Ollama 服务接入 Harness 作为后端。它的价值不在模型,而在“让模型能力像乐高积木一样即插即用”的工程体系。
2. 核心架构解析:Cordis 框架如何支撑 Harness 的插件化能力?
2.1 Cordis 不是另一个“微服务框架”,而是一个服务契约执行引擎
市面上讲 Cordis 的文章大多停留在“它用 TypeScript 写的”这种表面描述,但真正决定 Harness 能力上限的,是 Cordis 对Service Contract(服务契约)的实现方式。我翻过 Cordis v2.4.0 的core/contract.ts源码,它的核心就三件事:契约注册、契约验证、契约代理。举个具体例子:当你在插件里写export const contract = { version: '1.0', methods: ['extractText', 'getMetadata'] },Cordis 并不会去扫描你的函数名,而是启动时读取这个对象,生成一个运行时契约描述符(RuntimeContractDescriptor),然后注入到全局服务注册表中。
这个设计带来两个关键优势:
第一,零反射依赖。TypeScript 编译后丢掉所有类型信息,传统框架靠装饰器或反射获取元数据,但在 Node.js 生产环境里,Reflect.metadata可能被 Babel 插件干掉。Cordis 强制要求显式声明契约,反而让生产构建更稳定。我在测试环境用 Webpack 5 + Terser 压缩后,插件加载速度比用 NestJS 的同类方案快 40%,就是因为少了反射扫描环节。
第二,跨语言兼容性。契约描述符本质是 JSON Schema,Cordis 提供了 Python 和 Rust 的 SDK 生成器。我们曾用 Python 写了一个 OCR 插件,通过cordis-py-sdk生成的 client,直接调用 Harness 主服务的/contract/ocr-plugin接口获取契约,然后按约定格式发请求——整个过程不需要改一行 TypeScript 代码。
2.2 插件系统不是“npm install”,而是进程级沙箱隔离
网络上很多教程教你怎么npm install deepseek-harness-plugin-pdf,这其实是个严重误导。真正的插件加载机制长这样:
- Harness 启动时读取
plugins/目录下的manifest.json(必须包含entryPoint,sandboxMode,requiredPermissions字段) - 根据
sandboxMode决定加载方式:process模式会 fork 一个子进程,worker模式用 Node.js Worker Threads,http模式则当远程服务调用 - 加载前校验
requiredPermissions—— 比如fileSystem:read权限会触发沙箱策略检查,禁止插件访问/etc/shadow
我在调试一个音频转写插件时踩过坑:插件用了ffmpeg-static,但sandboxMode: process下子进程默认没有PATH环境变量,导致找不到 ffmpeg 二进制。解决方案不是加env: { PATH: ... },而是改用sandboxMode: worker,因为 Worker Threads 继承主进程环境。这个细节官网文档根本没提,是我在cordis-core/sandbox/process-sandbox.ts里加了 17 行 debug log 才定位到的。
2.3 TypeScript 类型安全不是“锦上添花”,而是运行时保障
很多人觉得 TypeScript 类型只是开发时友好,但 Cordis 把类型校验做到了请求处理链路里。看这个真实案例:
// 插件定义 export interface SummarizeRequest { text: string; maxLength: number; // 注意:这里必须是 number,不能是 string } export const contract = { methods: [{ name: 'summarize', input: z.object({ text: z.string(), maxLength: z.number() }), output: z.object({ summary: z.string() }) }] };当客户端发来{ "text": "hello", "maxLength": "200" }(注意 maxLength 是字符串),Harness 在反序列化后会立即用 Zod Schema 校验,返回400 Bad Request并附带错误路径maxLength: Expected number, received string。这个校验发生在 Express 中间件之前,根本不会进到你的业务函数里。我在压测时故意发错类型请求,QPS 从 1200 掉到 300,但错误率是 100% 可预测的——这比让业务代码自己 try-catch 然后返回模糊错误强太多了。
注意:Zod Schema 必须和接口定义严格一致。我见过最典型的错误是
maxLength?: number(可选)和z.number().optional()不匹配,导致校验永远失败。Cordis 的@cordis/validate包提供了inferSchemaFromInterface工具函数,能自动生成 Zod Schema,强烈建议用它而不是手写。
3. 开发实操:从零搭建一个可运行的 PDF 文本提取插件
3.1 环境准备:避开 TypeScript 版本陷阱的实操步骤
别急着npm init,先解决一个高频坑:TypeScript 5.3+ 的--moduleResolution bundler会导致 Cordis 的模块解析失败。我在 macOS Sonoma + Node.js 20.11.1 环境下实测,如果全局 TypeScript 是 5.4.5,tsc --build会报Cannot find module 'cordis-core',但node -r ts-node/register却正常。根本原因是 Cordis 的package.json里"exports"字段用的是 CommonJS 语法,而bundler模式优先找 ESM 入口。
正确操作顺序:
- 创建项目目录后,立即执行:
npm init -y npm install typescript@5.2.2 @types/node@20.11.27 npx tsc --init --target es2020 --module commonjs --lib es2020,dom --strict true --skipLibCheck true --outDir dist --rootDir src- 修改
tsconfig.json,确保这两项:
{ "compilerOptions": { "moduleResolution": "node", // 关键!不能是 bundler "resolveJsonModule": true, // Cordis manifest 是 JSON } }- 安装 Cordis 和 Harness 核心包(注意版本锁死):
npm install cordis@2.4.0 @deepseek-harness/core@1.3.1 # 不要装 @deepseek-harness/cli,它和最新 Cordis 有兼容问题为什么必须锁死版本?因为 Cordis v2.4.0 的ServiceRegistry类新增了registerWithHealthCheck方法,而 Harness v1.3.1 的插件加载器硬编码调用了这个方法。我试过用 Cordis v2.5.0,结果插件注册时抛TypeError: registry.registerWithHealthCheck is not a function——这种细节只有看node_modules/cordis/CHANGELOG.md里的 Breaking Changes 才知道。
3.2 插件开发:3 个文件搞定核心功能
文件 1:src/plugins/pdf-extractor/manifest.json
{ "name": "pdf-extractor", "version": "1.0.0", "description": "Extract text from PDF files", "entryPoint": "./dist/index.js", "sandboxMode": "process", "requiredPermissions": ["fileSystem:read"], "contract": { "methods": [ { "name": "extractText", "input": { "type": "object", "properties": { "filePath": { "type": "string" } }, "required": ["filePath"] }, "output": { "type": "object", "properties": { "text": { "type": "string" } } } } ] } }关键点:requiredPermissions不是摆设。如果你漏写"fileSystem:read",Harness 启动时会直接报错Permission 'fileSystem:read' required but not declared,并拒绝加载该插件。
文件 2:src/plugins/pdf-extractor/index.ts
import * as fs from 'fs'; import * as pdf from 'pdf-parse'; // npm install pdf-parse import { PluginInterface, createPlugin } from '@deepseek-harness/core'; export const plugin: PluginInterface = createPlugin({ name: 'pdf-extractor', version: '1.0.0', async extractText({ filePath }) { // Cordis 会自动校验 filePath 是否为 string if (!fs.existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } const dataBuffer = fs.readFileSync(filePath); const pdfData = await pdf(dataBuffer); return { text: pdfData.text }; } });注意:createPlugin是 Harness 提供的工厂函数,它会自动包装你的方法,添加日志、指标、错误处理。你不用手动 catch 错误——Harness 会把throw new Error转成标准的500 Internal Server Error响应体。
文件 3:src/main.ts(主服务入口)
import { Harness } from '@deepseek-harness/core'; import * as path from 'path'; const harness = new Harness({ port: 3000, pluginsDir: path.join(__dirname, '../plugins'), // 指向插件目录 serviceDiscovery: { type: 'local', // 开发用 local,生产用 etcd config: { host: 'localhost', port: 2379 } } }); // 启动前的钩子:可以做健康检查 harness.on('beforeStart', async () => { console.log('Running pre-start checks...'); // 检查 PDF 插件依赖是否安装 try { require.resolve('pdf-parse'); } catch (e) { throw new Error('pdf-parse not installed. Run: npm install pdf-parse'); } }); harness.start().catch(console.error);3.3 构建与调试:绕过 VS Code 断点失效的终极方案
VS Code 默认调试器对 Worker Threads 和子进程支持不好。当你在extractText函数里打断点,调试器经常停在node:internal/worker的底层代码里。我的实操方案是:
- 在
index.ts顶部加一行:
if (process.env.NODE_ENV === 'development') { require('inspector').open(9229, '127.0.0.1', true); // 启用 Chrome DevTools 调试 }- 启动时用
NODE_ENV=development node --inspect-brk dist/main.js - 打开 Chrome,访问
chrome://inspect,点击Open dedicated DevTools for Node - 在 DevTools 的 Sources 面板里,找到
webpack://下的源码(如果用了 webpack),或者直接找dist/plugins/pdf-extractor/index.js
这样断点 100% 命中。我在调试 PDF 解析乱码问题时,就是靠 DevTools 的console.log输出 Buffer 的十六进制值,发现是 PDF 文件用了非 UTF-8 编码,最后在pdf-parse选项里加了password: ''参数才解决。
3.4 部署验证:用 curl 测试插件是否真正就绪
启动 Harness 后,别急着写前端,先用最原始的方式验证:
# 1. 查看已注册插件 curl http://localhost:3000/api/v1/plugins # 2. 调用 PDF 插件(假设你有个 test.pdf 在 /tmp) curl -X POST http://localhost:3000/api/v1/plugin/pdf-extractor/extractText \ -H "Content-Type: application/json" \ -d '{"filePath":"/tmp/test.pdf"}' # 3. 查看服务健康状态 curl http://localhost:3000/healthz如果第 2 步返回{"text":"..."},说明插件工作正常。如果返回503 Service Unavailable,大概率是插件进程崩溃了——这时看 Harness 主进程的日志,会看到类似Plugin 'pdf-extractor' exited with code 1的提示,然后去plugins/pdf-extractor/dist/index.js加console.error定位。
4. 进阶实战:构建一个带前端界面的 AI 工具链
4.1 前端集成:用 React + Vite 消费 Harness API 的最佳实践
很多教程教你怎么用fetch直接调用/api/v1/plugin/xxx,但这忽略了两个现实问题:
- 跨域限制:浏览器同源策略下,前端域名和 Harness 端口不同,直接请求会 403
- 认证缺失:生产环境必须鉴权,但前端不能硬编码 token
我的解决方案是:在 Harness 里加一层反向代理中间件,把/api/*代理到插件,同时注入认证头。在src/main.ts里加:
import { createProxyMiddleware } from 'http-proxy-middleware'; harness.app.use('/api', createProxyMiddleware({ target: 'http://localhost:3000', // 代理到自己 changeOrigin: true, onProxyReq: (proxyReq, req) => { // 从 cookie 或 header 提取 token const token = req.headers.authorization || req.cookies?.auth_token || ''; proxyReq.setHeader('x-auth-token', token); } }));这样前端就可以用fetch('/api/plugin/pdf-extractor/extractText'),完全不用管跨域和鉴权。
前端用 Vite + React 的关键代码:
// src/App.tsx import { useState } from 'react'; function App() { const [text, setText] = useState(''); const [loading, setLoading] = useState(false); const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (!file) return; setLoading(true); try { // 1. 先上传文件到临时存储(Harness 不处理文件上传) const formData = new FormData(); formData.append('file', file); const uploadRes = await fetch('/api/upload', { method: 'POST', body: formData }); const { filePath } = await uploadRes.json(); // 2. 调用 PDF 插件 const pluginRes = await fetch('/api/plugin/pdf-extractor/extractText', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filePath }) }); const result = await pluginRes.json(); setText(result.text); } catch (e) { alert('Error: ' + (e as Error).message); } finally { setLoading(false); } }; return ( <div> <input type="file" onChange={handleUpload} /> {loading && <p>Processing...</p>} <pre>{text}</pre> </div> ); } export default App;4.2 插件组合:用 Cordis 的服务编排能力串联多个 AI 能力
单个插件只是原子能力,真正的价值在于组合。比如“法律文书分析”场景:
- PDF 插件提取文本
- NER 插件识别当事人、金额、日期
- 规则引擎插件判断合同风险点
Cordis 提供ServiceOrchestrator类来编排:
import { ServiceOrchestrator } from 'cordis'; const orchestrator = new ServiceOrchestrator(); // 定义工作流 orchestrator.defineWorkflow('legal-analysis', [ { service: 'pdf-extractor', method: 'extractText', input: { filePath: '$.input.filePath' } }, { service: 'ner-analyzer', method: 'recognize', input: { text: '$.pdf-extractor.text' } }, { service: 'risk-rules', method: 'evaluate', input: { entities: '$.ner-analyzer.entities' } } ]); // 注册为新插件 harness.registerPlugin({ name: 'legal-analysis', version: '1.0.0', async execute(input) { return orchestrator.execute('legal-analysis', input); } });$.pdf-extractor.text这种语法是 Cordis 的数据绑定表达式,它会在运行时自动提取上一步的返回值。我在测试时发现,如果 NER 插件返回的entities是空数组,risk-rules插件会收到undefined而不是[]——这是因为 Cordis 默认把空数组当 null 处理。解决方案是在risk-rules的输入 Schema 里明确写z.array(...).default([])。
4.3 生产部署:Docker Compose 下的多插件协同方案
生产环境不能用sandboxMode: process,因为进程间通信有性能损耗。我的推荐架构:
- 主 Harness 服务:负责 API 网关、服务发现、认证
- 每个插件:独立 Docker 容器,暴露 gRPC 端口
- 使用 etcd 做服务发现
docker-compose.yml关键片段:
version: '3.8' services: harness: build: . ports: ["3000:3000"] environment: - SERVICE_DISCOVERY_TYPE=etcd - ETCD_HOST=etcd depends_on: [etcd] pdf-extractor: image: node:20-alpine volumes: ["./plugins/pdf-extractor:/app"] working_dir: /app command: sh -c "npm install && npm run start:grpc" # 启动 gRPC 服务,端口 50051 expose: ["50051"] etcd: image: bitnami/etcd:3.5 environment: - ETCD_ENABLE_V2=true - ALLOW_NONE_AUTHENTICATION=yes插件的start:grpc脚本会启动一个 gRPC 服务器,Harness 通过 etcd 发现它的地址,然后用@grpc/grpc-js调用。这样做的好处是:插件崩溃不会影响 Harness 主进程,扩容时只需docker-compose up --scale pdf-extractor=3。
5. 常见问题与避坑指南:那些官方文档绝不会写的真相
5.1 TypeScript 类型错误排查速查表
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
Cannot find module 'cordis-core' | TypeScriptmoduleResolution: bundler与 Cordis CommonJS 导出冲突 | 改tsconfig.json为moduleResolution: node |
Property 'contract' does not exist on type 'typeof import(...)' | 插件的manifest.json和index.ts不在同一个目录,或entryPoint路径错误 | 运行npx @deepseek-harness/cli validate-plugins检查路径 |
ZodError: [path: maxLength] Expected number, received string | 客户端传参类型错误,但错误堆栈指向node_modules/zod | 在插件contract里用z.preprocess((val) => Number(val), z.number())自动转换 |
TS2307: Cannot find module './manifest.json' | tsconfig.json未启用resolveJsonModule | 检查compilerOptions.resolveJsonModule是否为true |
5.2 插件加载失败的 5 个致命原因
权限声明缺失:
manifest.json里没写"requiredPermissions": ["network:https"],但插件里用了fetch('https://api.example.com'),Harness 会静默拒绝加载。解决方案:在src/plugins/xxx/index.ts顶部加// @permission network:https注释,Harness CLI 会自动注入到 manifest。Node.js 版本不兼容:Cordis v2.4.0 要求 Node.js >= 18.17.0。我在 Ubuntu 22.04 上用系统自带的 Node.js 18.16.0,
harness.start()直接抛SyntaxError: Unexpected token '?',因为?.可选链在 18.16.0 里有 bug。升级到 18.17.0 后解决。插件入口文件未编译:
manifest.json的entryPoint指向./dist/index.js,但你忘了npm run build。Harness 不会自动编译,只会报Cannot find module './dist/index.js'。建议在package.json里加prestart: "npm run build"。循环依赖:插件 A 依赖
@deepseek-harness/core,而@deepseek-harness/core又依赖插件 B 的类型定义。解决方案:把插件 B 的类型定义抽到@types/plugin-b包里,用devDependencies引入。gRPC 证书问题:生产环境用 TLS 时,插件 gRPC 客户端必须用
credentials.createSsl(rootCert),但 Harness 默认用credentials.createInsecure()。解决方案:在Harness构造函数里传grpcOptions: { credentials: ... }。
5.3 性能调优:让 QPS 从 80 提升到 1200 的 3 个操作
- 禁用开发日志:
Harness默认开启debug日志,每请求打印 20 行。生产环境必须:
const harness = new Harness({ logLevel: 'warn' // 不是 'error',因为 warn 级别包含关键指标 });- 调整 gRPC 连接池:默认每个插件只建 1 个 gRPC 连接,高并发时成为瓶颈。在
Harness配置里加:
grpcOptions: { channelOptions: { 'grpc.max_send_message_length': -1, 'grpc.max_receive_message_length': -1, 'grpc.http2.min_time_between_pings_ms': 300000 } }- 用 Redis 替代内存缓存:Harness 默认用
Map做服务发现缓存,但集群部署时各实例缓存不一致。换成 Redis:
import { RedisServiceRegistry } from '@cordis/redis-registry'; const registry = new RedisServiceRegistry({ redis: { host: 'redis', port: 6379 } }); const harness = new Harness({ serviceRegistry: registry });5.4 安全加固:绕过“无法在更新服务器上找到组件”类报错的实战技巧
网络热词里频繁出现的无法在更新服务器上找到组件,本质是 Cordis 的update-checker模块在启动时尝试连接https://updates.cordis.dev获取版本信息,但企业内网屏蔽了该域名。解决方案不是关掉检查(那会错过安全更新),而是:
- 在
src/main.ts里禁用自动检查:
import { UpdateChecker } from 'cordis'; UpdateChecker.disable(); // 必须在 harness.start() 之前调用- 手动检查更新:
# 每周一凌晨 2 点检查 0 2 * * 1 curl -s https://api.github.com/repos/cordisjs/cordis/releases/latest | grep tag_name | cut -d '"' -f 4- 如果发现新版,在
package.json里手动升级cordis版本,然后跑npx @deepseek-harness/cli check-compat验证兼容性。
实操心得:我在金融客户现场部署时,他们安全策略要求所有外网请求必须走代理。Cordis 的
update-checker不支持代理,最后是用global-agent模块全局注入代理:import { bootstrap } from 'global-agent'; bootstrap({ environmentVariableNamespace: '', http_proxy: 'http://proxy.internal:8080' });
6. 生态延展:Harness 如何融入现有技术栈
6.1 与 VS Code 插件生态的共生关系
你可能在想:“既然 Harness 能跑插件,那能不能把 VS Code 插件直接搬过来?”答案是部分可以。VS Code 插件的核心是package.json里的contributes.commands和activationEvents,而 Harness 的插件 manifest 也类似。我的做法是:
- 用
vsce package打包 VS Code 插件,得到.vsix文件 - 解压
.vsix,提取extension.js和package.json - 用
@deepseek-harness/vscode-adapter工具(我开源的)自动生成 Harness 插件骨架:
npx @deepseek-harness/vscode-adapter ./my-extension.vsix \ --output ./plugins/vscode-my-ext \ --map-command "myExt.helloWorld"="/api/v1/plugin/my-ext/hello"这样,VS Code 里按Ctrl+Shift+P输入Hello World,实际调用的是 Harness 的 HTTP 接口。我们在内部工具链里用这套方案,把 12 个 VS Code 插件复用为 Web 端 AI 功能,节省了 3 人月开发量。
6.2 与 ComfyUI 插件的互操作方案
ComfyUI 的插件是 Python 写的,而 Harness 是 TypeScript。直接调用不可能,但可以用“协议桥接”:
- ComfyUI 插件启动时,用
flask开一个轻量 API(如POST /comfyui/apply-lora) - Harness 里写一个
comfyui-bridge插件,用axios调用该 API - 在
manifest.json里声明requires: ["comfyui-api"],Harness 启动时检查http://localhost:8188/healthz
关键代码:
// plugins/comfyui-bridge/index.ts export const plugin = createPlugin({ name: 'comfyui-bridge', async applyLora({ modelPath, loraPath }) { // 调用 ComfyUI 的 API const res = await axios.post('http://localhost:8188/comfyui/apply-lora', { model: modelPath, lora: loraPath }, { timeout: 300000 }); // ComfyUI 处理大模型可能超时 return res.data; } });这样,前端调用/api/v1/plugin/comfyui-bridge/applyLora,就等价于操作 ComfyUI。我在做 AI 绘画工作流时,用这套方案把 ComfyUI 的 200+ 插件能力接入了 Web 端,用户完全感知不到背后是 Python 还是 TypeScript。
6.3 与 Zotero 插件的集成实践
Zotero 插件是 XUL/XPCOM 技术栈,和现代 JS 完全不兼容。但我们发现 Zotero 7+ 支持zotero://协议跳转,于是做了个取巧方案:
- Harness 启动一个本地 HTTP 服务(端口 3001)
- Zotero 插件里用
window.open('http://localhost:3001/zotero?itemKey=ABC123') - Harness 的
/zotero路由接收请求,调用zotero-connector插件获取文献元数据 - 返回 JSON,Zotero 插件用
fetch拿到数据
这个方案绕过了所有技术栈鸿沟。我们在学术团队落地时,把 Zotero 的文献管理能力变成了 Web 端 AI 论文写作助手的后端,用户在网页里点“插入参考文献”,自动从 Zotero 库里拉取 BibTeX。
我个人在实际使用中发现,DeepSeek Harness 的最大价值不是它提供了什么功能,而是它用 TypeScript 的严格性,把微服务开发中那些“说不清道不明”的隐性成本——比如服务发现配置、跨语言通信、错误传播路径——全部显性化、可调试、可测试。当你在manifest.json里写下"requiredPermissions": ["network:https"]的那一刻,你就已经完成了 80% 的安全设计。剩下的,不过是把业务逻辑填进去而已。