[
  {
    "path": ".dockerignore",
    "content": "node_modules\nrelease\nlib\n\n"
  },
  {
    "path": ".eslintignore",
    "content": "lib/\ndist/\ncoverage/\nnode_modules/\nchrome/js/icon.js\nreleases/\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  extends: ['@diamondyuan/react-typescript', 'prettier'],\n  plugins: ['eslint-plugin-prettier'],\n  rules: {\n    'no-use-before-define': 'off',\n    'arrow-body-style': 'off',\n    'no-redeclare': 'off',\n    'prettier/prettier': 'error',\n    '@typescript-eslint/no-unused-vars': 'off',\n  },\n  settings: {\n    'import/resolver': {\n      webpack: {\n        config: './webpack/webpack.common.js',\n      },\n    },\n  },\n};\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report_en-US.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\n---\n\n**Describe the bug**\n\nA clear and concise description of what the bug is.\n\n**To Reproduce**\n\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\n\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\n\nIf applicable, add screenshots to help explain your problem.\n\n**please complete the following information**\n\n- Notebook: [e.g. notion,yuque]\n- Browser [e.g. chrome, safari]\n- Version [e.g. 1.23.0]\n\n**Additional context**\n\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report_zh-CN.md",
    "content": "---\nname: Bug 报告\nabout: 创建报告以帮助我们改进\n---\n\n**Bug 描述**\n\n清楚简明地描述错误是什么。\n\n**复现步骤**\n\n重现的步骤：\n\n1. 打开 '...'\n2. 点击按钮 '....'\n3. 滚动到 '....'\n4. 看到错误\n\n**预期行为**\n\n对您期望发生的事情的简洁明了的描述。\n\n**截图**\n\n如果适用，请添加屏幕截图以帮助解释您的问题。\n\n**请填写以下信息**\n\n- 笔记平台: [e.g. notion,yuque]\n- 浏览器 [e.g. chrome, safari]\n- 版本 [e.g. 1.23.0]\n\n**其他背景**\n\n在此处添加有关该问题的任何其他上下文。\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request_en-US.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\n---\n\n**Is your feature request related to a problem? Please describe.**\n\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\n\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\n\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\n\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request_zh-CN.md",
    "content": "---\nname: 功能请求\nabout: 为这个项目提出一个想法\n---\n\n**您的功能要求与问题有关吗？ 请描述。**\n\n清楚，简洁地说明问题所在。 例如 当[...]时，我总是感到沮丧\n\n**描述您想要的解决方案**\n\n对您想要发生的事情的简洁明了的描述。\n\n**描述您考虑过的替代方案**\n\n对您考虑过的所有替代解决方案或功能的简洁明了的描述。\n\n**其他内容**\n\n在此处添加有关功能请求的其他任何上下文或屏幕截图。\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI Test\n\non:\n  pull_request_target:\n    types: [opened, edited, reopened]\n  push:\n    branches:\n      - '**'\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n        with:\n          fetch-depth: 0\n      - uses: actions/setup-node@v1\n        with:\n          node-version: '16.x'\n      - name: Install Dependencies\n        run: npm install --force\n      - run: npm run cov\n        env:\n          GITHUB_BRANCH: ${{ github.ref }}\n"
  },
  {
    "path": ".gitignore",
    "content": "npm-debug.log\nnode_modules/\ndist/*\n!dist/.gitkeep\nlib/\ncoverage/\ntmp/\n.DS_Store\n.idea\n.vscode\nyarn-error.log\ndll/\nwebclipper.zip\n\n.now\nrelease\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"es5\",\n  \"printWidth\": 100,\n  \"proseWrap\": \"never\"\n}\n"
  },
  {
    "path": ".yarnrc",
    "content": "version-git-message \"chore(release): %s :tada:\"\n"
  },
  {
    "path": "LICENSE",
    "content": "Software License Agreement\nCopyright (c) 2020-2020, DiamondYuan. All rights reserved.\n\nLicensed under the terms of GNU General Public License Version 2 or later.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">Web Clipper</h1>\n<p align=\"center\">\n    <a href=\"https://github.com/webclipper/web-clipper/actions\">\n      <img src=\"https://github.com/webclipper/web-clipper/workflows/CI%20Test/badge.svg\" alt=\"CI Test Status\">\n    </a>\n     <a href=\"https://github.com/webclipper/web-clipper/actions\">\n      <img src=\"https://github.com/webclipper/web-clipper/workflows/Release resource/badge.svg\" alt=\"Release resource status\">\n    </a>\n    <a href=\"https://codecov.io/gh/webclipper/web-clipper\">\n      <img src=\"https://img.shields.io/codecov/c/github/webclipper/web-clipper/master.svg?style=flat-square\" alt=\"Codecov\">\n    </a>\n</p>\n\nYou can use Web Clipper to save anything on the web to anywhere.\n\n<img src=\"https://clipper.website/static/image/screenshot.png\">\n\n### Support Site\n\n- [FlowUs](https://flowus.cn/)\n- [Obsidian](https://obsidian.md/)\n- [Github](https://github.com)\n- [Yuque](https://www.yuque.com)\n- [Buildin.AI](https://buildin.ai/product)\n- [Notion](https://www.notion.so/)\n- [Youdao](https://note.youdao.com/)\n- [OneNote](https://www.onenote.com/)\n- [Bear](https://bear.app)\n- [Joplin](https://joplinapp.org/)\n- [Server Chan](http://sc.ftqq.com/3.version)\n- [dida365](https://dida365.com/)\n- [baklib](https://www.baklib-free.com/)\n- [wolai](https://www.wolai.com/)\n- [Leanote](https://github.com/leanote/leanote)\n- [Flomo](https://flomoapp.com/)\n- [Siyuan](https://b3log.org/siyuan)\n- [Ulysses](https://ulysses.app/)\n- [Confluence](https://www.atlassian.com/software/confluence)\n\n### Install\n\n- [Chrome](https://chrome.google.com/webstore/detail/web-clipper/mhfbofiokmppgdliakminbgdgcmbhbac)\n\n- [Edge](https://microsoftedge.microsoft.com/addons/detail/opejamnnohhbjflpbhnmdlknhjkfhfdp)\n\nps: Because the review takes a week, the version will fall behind.\n\n#### From Github\n\n1. Download the webclipper.zip from [release page](https://github.com/webclipper/web-clipper/releases)\n2. Go to **chrome://extensions/** and check the box for **Developer mode** in the top right.\n3. Locate the ZIP file on your computer and unzip it.\n4. Go back to the chrome://extensions/ page and click the **Load unpacked extension** button and select the unzipped folder for your extension to install it.\n\n### Develop\n\n```bash\n$ git clone https://github.com/webclipper/web-clipper.git\n$ cd web-clipper\n$ npm i\n$ npm run dev\n```\n\n- You should load the 'dist/chrome' folder in Chrome.\n\n- You should load the 'dist/manifest.json' folder in Firefox.\n\n### Test\n\n```bash\n$ npm run test\n```\n\n### Feedback\n\n| Type     | Link                                                 |\n| -------- | ---------------------------------------------------- |\n| Telegram | [Link](https://t.me/joinchat/HoVttRRUIA6aXASixzoqAw) |\n"
  },
  {
    "path": "bin/index.js",
    "content": "#!/usr/bin/env node\nrequire('ts-node').register({\n  transpileOnly: true,\n});\nrequire('./main');\n"
  },
  {
    "path": "bin/main.ts",
    "content": "import { hideBin } from 'yargs/helpers';\nimport { format } from './scripts';\n\nconst [command] = hideBin(process.argv);\n\nswitch (command) {\n  case 'format': {\n    format();\n    break;\n  }\n  default: {\n    throw new Error('unknown command');\n  }\n}\n"
  },
  {
    "path": "bin/scripts/format.ts",
    "content": "import * as fs from 'fs';\nimport * as path from 'path';\n\nexport function format() {\n  const localsPath = path.resolve(__dirname, '../../src/common/locales/data');\n  const files = fs.readdirSync(localsPath);\n\n  const sortedKeys = Object.keys(\n    JSON.parse(fs.readFileSync(path.resolve(localsPath, 'en-US.json'), { encoding: 'utf-8' }))\n  ).sort((a, b) => a.localeCompare(b));\n\n  files\n    .filter(file => path.extname(file) === '.json')\n    .map(file => path.resolve(localsPath, file))\n    .forEach(file => {\n      const messages = JSON.parse(fs.readFileSync(file, 'utf-8'));\n      const result = {};\n\n      sortedKeys.forEach(key => {\n        result[key] = messages[key] || '';\n      });\n\n      fs.writeFileSync(file, JSON.stringify(result, null, 2));\n    });\n}\n"
  },
  {
    "path": "bin/scripts/index.ts",
    "content": "export { format } from './format';\n"
  },
  {
    "path": "chrome/html/error.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Plugin Installation Notice</title>\n  <style>\n    body {\n      font-family: Arial, sans-serif;\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      height: 100vh;\n      margin: 0;\n      background-color: #f4f4f9;\n    }\n\n    .container {\n      text-align: center;\n      padding: 20px;\n      border: 1px solid #ddd;\n      border-radius: 8px;\n      background-color: #fff;\n      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n      max-width: 400px;\n      width: 100%;\n    }\n\n    h1 {\n      font-size: 24px;\n      color: #333;\n    }\n\n    p {\n      font-size: 16px;\n      color: #666;\n      line-height: 1.5;\n    }\n\n    a {\n      color: #007bff;\n      text-decoration: none;\n    }\n\n    a:hover {\n      text-decoration: underline;\n    }\n  </style>\n</head>\n\n<body>\n  <div class=\"container\">\n    <h1>Plugin Installation Notice</h1>\n    <p>\n      After installing the plugin, if you want to use it on pages that were already open before installation, please\n      refresh the page.\n    </p>\n  </div>\n</body>\n\n</html>\n"
  },
  {
    "path": "chrome/js/icon.js",
    "content": "window._iconfont_svg_string_1402208='<svg><symbol id=\"obsidian\" viewBox=\"0 0 1024 1024\"><path d=\"M574.34 7.74l-313.83 168-127.71 268.8 194.04 432 275.22 91.2 49.5-97.92L756.5 261.18 574.34 7.74z\" fill=\"#34208C\" ></path><path d=\"M757.31 260.46l-194.04-115.2-267.3 204.48 306.9 617.28 49.5-97.92 104.94-608.64z\" fill=\"#6C56CC\" ></path><path d=\"M757.13 261.08L574.97 7.64l-11.88 138.24 194.04 115.2z\" fill=\"#AF9FF4\" ></path><path d=\"M562.82 146.02L574.7 7.78l-313.83 168 34.65 174.72 267.3-204.48zM295.9 350.66l31.68 526.08 275.22 91.2-306.9-617.28z\" fill=\"#4A37A0\" ></path></symbol><symbol id=\"siyuan\" viewBox=\"0 0 1024 1024\"><path d=\"M277.344 89.984c-7.072 7.008-12.672 13.344-12.48 14.016s-0.128 0.96-0.704 0.64c-1.504-0.928-12.704 10.784-11.584 12.032 0.512 0.576 0.352 0.704-0.416 0.256-1.44-0.8-7.808 5.536-7.808 7.776 0 0.736-0.416 1.056-0.928 0.736-1.184-0.736-4.064 2.368-3.072 3.328 0.416 0.416 0.064 0.736-0.736 0.736-1.92 0-7.488 5.568-7.488 7.488 0 0.8-0.32 1.152-0.736 0.768-0.768-0.768-10.752 8.448-24.096 22.144-22.4 23.04-41.152 41.312-41.728 40.768-0.384-0.384-0.672 0.288-0.672 1.472s-0.544 1.76-1.216 1.344c-1.12-0.672-1.76 0.928-1.344 3.36 0.064 0.512-0.544 0.768-1.376 0.608s-1.376 0.416-1.184 1.312c0.192 0.896-0.192 1.312-0.8 0.928s-4.096 2.4-7.68 6.176c-10.112 10.656-23.04 23.488-32.768 32.544-4.864 4.544-8.48 8.256-8.032 8.256s-0.96 1.504-3.2 3.36c-7.968 6.56-22.048 21.696-23.328 25.056-0.448 1.184-1.664 2.144-2.688 2.144s-1.888 0.864-1.888 1.888-0.448 1.632-0.96 1.312c-1.248-0.768-23.616 20.704-22.592 21.696 0.416 0.416 0.064 0.768-0.768 0.768s-4.288 2.88-7.648 6.4c-16.064 16.768-25.664 26.592-26.048 26.592-0.224 0-5.12 4.704-10.912 10.432l-10.496 10.432v289.024c0 197.632 0.384 289.76 1.248 291.392 1.856 3.456 3.776 2.848 7.904-2.432 2.016-2.592 4.096-4.448 4.576-4.128s0.928-0.416 0.928-1.568 0.288-1.824 0.672-1.44c0.64 0.64 92.768-90.208 157.888-155.712 5.536-5.568 25.472-25.472 44.288-44.192s33.856-34.624 33.408-35.328c-0.416-0.704-0.256-0.928 0.384-0.544 1.664 1.024 8-5.088 6.976-6.72-0.48-0.768-0.256-0.992 0.512-0.512 1.376 0.864 6.368-3.648 6.368-5.792 0-0.64 0.288-0.896 0.672-0.544 2.272 2.304 27.616-26.656 26.944-30.816-0.384-2.368-0.704-133.92-0.704-292.352s-0.416-288.032-0.928-287.968-6.688 5.856-13.728 12.864z m433.504 2.656c-8.576 8.576-15.584 16.32-15.584 17.28s-0.352 1.344-0.736 0.928c-0.576-0.608-39.328 37.28-88.48 86.496l-48.864 49.024-43.968 44.128v292.8c0 161.056 0.288 292.8 0.672 292.8 1.344 0 6.496-5.856 5.792-6.592-0.416-0.416 0.032-0.736 0.992-0.768s5.312-3.968 9.664-8.8c4.352-4.8 11.456-12.064 15.744-16.16s15.584-15.296 25.056-24.96c9.472-9.664 20.704-20.928 24.96-25.056 21.728-21.12 36.896-36.224 36.448-36.224-0.288 0 4.512-5.12 10.656-11.392s11.584-11.2 12.096-10.912 0.928-0.48 0.928-1.632 0.352-1.76 0.736-1.344c0.928 0.928 4.128-2.144 4.128-4 0-0.736 0.416-1.152 0.928-0.864 0.864 0.512 7.36-5.44 6.72-6.144-0.16-0.192 0.096-0.448 0.608-0.576 1.696-0.448 3.712-2.816 3.168-3.744-0.32-0.512 0.352-0.928 1.44-0.928s1.632-0.544 1.216-1.216c-0.416-0.672-0.256-1.216 0.352-1.216s13.024-11.808 27.584-26.208l26.464-26.208v-290.176c0-159.616-0.352-291.04-0.736-292.096-1.152-3.008-1.312-2.848-17.888 13.696z\" fill=\"#D23E31\" ></path><path d=\"M292.032 75.776c0 0.672 0.832 1.216 1.824 1.216s1.824-0.544 1.824-1.216c0-0.672-0.832-1.216-1.824-1.216s-1.824 0.544-1.824 1.216z m3.264 2.016c-0.448 0.448-0.8 132.288-0.768 292.992l0.064 292.16 52.032 51.328c43.52 42.912 109.12 107.712 135.136 133.44 3.52 3.488 11.104 11.296 16.8 17.344s11.072 11.04 11.904 11.072c1.216 0.064 1.536-59.232 1.536-292.64V290.784l-5.632-5.152a411.008 411.008 0 0 1-13.152-12.8c-6.528-6.624-24.256-24.416-49.824-49.952-3.936-3.936-14.88-15.04-24.288-24.608s-17.088-17.12-17.088-16.736-3.712-3.2-8.256-7.968a1937.92 1937.92 0 0 0-27.2-27.52c-20.928-20.832-25.6-25.536-51.104-51.328-17.184-17.376-18.56-18.528-20.192-16.896z m436.192 1.92c-0.416 1.504-0.736 133.248-0.736 292.768v290.016l143.584 143.488c113.824 113.728 144.224 143.488 146.656 143.488h3.04V366.496l-144.8-144.768c-79.648-79.616-145.28-144.768-145.888-144.768s-1.408 1.248-1.824 2.752z\" fill=\"#3B3E43\" ></path><path d=\"M729.472 76.672c-0.544 104.512-0.32 585.248 0.288 584.608 1.184-1.184 1.824-585.536 0.608-585.536a0.928 0.928 0 0 0-0.928 0.928zM292.128 366.304c0.064 159.104 0.544 290.688 1.088 292.352s1.152-128.512 1.408-289.312c0.352-233.28 0.128-292.352-1.088-292.352s-1.504 58.592-1.408 289.312z m220.448 217.184c0 161.312 0.16 227.104 0.32 146.24s0.192-212.832 0-293.28c-0.192-80.416-0.32-14.272-0.32 147.04z m180.224 113.952l-7.904 8.256 8.256-7.904c7.68-7.328 8.8-8.608 7.904-8.608-0.192 0-3.904 3.712-8.256 8.256zM131.616 234.624c-1.152 1.344-1.664 2.432-1.152 2.432s1.888-1.12 3.04-2.432c1.152-1.344 1.664-2.432 1.152-2.432s-1.888 1.12-3.04 2.432zM82.496 283.424c-0.448 0.736-0.384 1.312 0.192 1.312s0.16 0.544-0.832 1.216c-1.28 0.832-1.344 1.184-0.256 1.216 0.864 0 1.888-0.832 2.272-1.888 0.864-2.272-0.192-3.712-1.344-1.856z m560.64 462.816c-6.176 6.272-10.816 11.584-10.4 11.84s5.856-4.704 12.064-10.944c6.208-6.24 10.88-11.584 10.4-11.84s-5.92 4.672-12.064 10.944zM121.472 833.696l-3.584 3.968 3.968-3.584a30.816 30.816 0 0 0 3.968-3.968c0-0.928-0.992-0.128-4.352 3.584zM7.904 945.824c-1.216 1.344-1.92 2.432-1.6 2.432s1.888-1.12 3.424-2.432 2.272-2.432 1.6-2.432c-0.672 0-2.208 1.12-3.424 2.432z\"  ></path></symbol><symbol id=\"leanote\" viewBox=\"0 0 1024 1024\"><path d=\"M900.98255 0H123.01745C55.417206 0 0.187431 55.354728 0.187431 122.830018v778.215009c0 67.600244 55.229774 122.830018 122.830019 122.830019h777.9651c67.600244 0 122.830018-55.229774 122.830019-122.830019V122.830018c0-67.47529-55.229774-122.830018-122.830019-122.830018zM675.815009 798.707505L532.492495 901.045027h-40.98499l-143.322514-102.337522V552.922514l102.337523 20.492495h143.322513l81.845028-20.492495v245.784991z m143.322514-552.922514c-40.984991 0 0-20.492495-81.845028-20.492496-61.477486 0-122.830018 143.322514-122.830018 143.322514 40.984991 0 61.477486 40.984991 61.477486 40.984991v102.337523s-81.845027 20.492495-163.815009 20.492495c-81.845027 0-163.815009-20.492495-163.815009-20.492495v-102.337523S368.802441 368.615009 409.787431 368.615009c0 0-61.477486-143.322514-122.830018-143.322514-81.845027 0-67.600244 20.492495-81.845027 20.492496-40.984991 0-47.107749-102.337523 0-102.337523 204.675046 0 225.167541 204.8 225.167541 204.8h163.815009s20.492495-204.8 225.167541-204.8c34.612325-0.124954 40.860037 102.337523-0.124954 102.337523z\"  ></path></symbol><symbol id=\"flomo\" viewBox=\"0 0 1024 1024\"><path d=\"M0 0h1024v1024H0z\" fill=\"#FAFAFA\" ></path><path d=\"M709.461333 507.211852H332.069926V399.559111H779.567407l-65.422222 105.263408c0 2.389333-2.341926 2.389333-4.683852 2.389333zM807.604148 339.749926H450.066963L515.508148 234.477037c2.341926 0 4.67437-2.389333 7.016296-2.389333H877.700741l-65.422222 105.263407c0 2.398815-2.341926 2.398815-4.683852 2.398815z\" fill=\"#30CF79\" ></path><path d=\"M337.910519 791.912296c-105.159111 0-191.620741-88.519111-191.620741-196.181333 0-107.662222 86.46163-196.171852 191.620741-196.171852 105.14963 0 191.620741 88.50963 191.62074 196.171852s-86.471111 196.171852-191.62074 196.171852z m0-282.311111c-46.743704 0-86.471111 38.276741-86.471112 88.519111 0 47.853037 37.394963 88.528593 86.471112 88.528593 49.066667 0 86.46163-38.286222 86.461629-88.528593-2.341926-50.24237-39.727407-88.519111-86.471111-88.519111z\" fill=\"#30CF79\" ></path></symbol><symbol id=\"github_repository\" viewBox=\"0 0 1024 1024\"><path d=\"M881 112c17.673 0 32 14.327 32 32v736c0 17.673-14.327 32-32 32H144c-17.673 0-32-14.327-32-32V144c0-17.673 14.327-32 32-32h737z m-564.5 72H184v656h132.5V184z m524.5 0h-56.032v286.08c0 8.837-7.163 16-16 16a16 16 0 0 1-9.008-2.777l-87.009-59.275-88.035 59.085c-7.337 4.924-17.277 2.968-22.201-4.369a16 16 0 0 1-2.715-8.916V184H388.5v656H841V184z m-120.032 0H624v195.804l49.22-33.033 47.748 32.528V184z\"  ></path></symbol><symbol id=\"wolai\" viewBox=\"0 0 1024 1024\"><path d=\"M736.08853333 85.33333333c70.44053333 0 95.984 7.3344 121.73653334 21.1072 25.75146667 13.77173333 45.96266667 33.98293333 59.7344 59.7344l2.43306666 4.69013334C932.1952 195.18826667 938.66666667 221.73973333 938.66666667 287.91146667v448.17706666l-0.08106667 12.32213334c-0.832 61.216-8.08746667 85.2224-21.02613333 109.4144-13.77173333 25.75146667-33.98293333 45.96266667-59.7344 59.7344l-4.69013334 2.43306666C828.81173333 932.1952 802.26026667 938.66666667 736.08853333 938.66666667H287.91146667l-12.32213334-0.08106667c-61.216-0.832-85.2224-8.08746667-109.4144-21.02613333-25.75146667-13.77173333-45.96266667-33.98293333-59.7344-59.7344l-2.43306666-4.69013334c-11.808-23.5392-18.25066667-49.1648-18.65386667-110.76586666L85.33333333 287.91146667c0-70.44053333 7.3344-95.984 21.1072-121.73653334 13.77173333-25.75146667 33.98293333-45.96266667 59.7344-59.7344l4.69013334-2.43306666c23.5392-11.808 49.1648-18.25066667 110.76586666-18.65386667L736.08853333 85.33333333z\" fill=\"#FFFFFF\" ></path><path d=\"M736.08853333 85.33333333c70.44053333 0 95.984 7.3344 121.73653334 21.1072 25.75146667 13.77173333 45.96266667 33.98293333 59.7344 59.7344l2.43306666 4.69013334C932.1952 195.18826667 938.66666667 221.73973333 938.66666667 287.91146667v448.17706666l-0.08106667 12.32213334c-0.832 61.216-8.08746667 85.2224-21.02613333 109.4144-13.77173333 25.75146667-33.98293333 45.96266667-59.7344 59.7344l-4.69013334 2.43306666C828.81173333 932.1952 802.26026667 938.66666667 736.08853333 938.66666667H287.91146667l-12.32213334-0.08106667c-61.216-0.832-85.2224-8.08746667-109.4144-21.02613333-25.75146667-13.77173333-45.96266667-33.98293333-59.7344-59.7344l-2.43306666-4.69013334c-11.808-23.5392-18.25066667-49.1648-18.65386667-110.76586666L85.33333333 287.91146667c0-70.44053333 7.3344-95.984 21.1072-121.73653334 13.77173333-25.75146667 33.98293333-45.96266667 59.7344-59.7344l4.69013334-2.43306666c23.5392-11.808 49.1648-18.25066667 110.76586666-18.65386667L736.08853333 85.33333333z m5.03466667 94.82453334H282.8768l-13.63413333 0.144c-34.95786667 0.6528-46.58453333 3.4528-58.35413334 9.7472-9.2288 4.93546667-15.904 11.61173333-20.84053333 20.84053333-7.05066667 13.18186667-9.71626667 26.18453333-9.8912 71.98826667v458.24426666l0.144 13.63413334c0.6528 34.95786667 3.4528 46.58453333 9.7472 58.35413333 4.93546667 9.2288 11.61173333 15.904 20.84053333 20.84053333 13.18186667 7.05066667 26.18453333 9.71626667 71.98826667 9.8912l453.21066667 0.01066667c49.90933333 0 63.36853333-2.60053333 77.02186666-9.90186667 9.2288-4.93546667 15.904-11.61173333 20.84053334-20.84053333 7.05066667-13.18186667 9.71626667-26.18453333 9.8912-71.98826667l0.01066666-453.21066666c0-49.90933333-2.60053333-63.36853333-9.90186666-77.02186667-4.93546667-9.2288-11.61173333-15.904-20.84053334-20.84053333-13.18186667-7.05066667-26.18453333-9.71626667-71.98826666-9.8912z m-406.90133333 431.39733333c43.20106667 0 78.22293333 35.02186667 78.22293333 78.22293333 0 43.2-35.02186667 78.22186667-78.22293333 78.22186667-43.2 0-78.22186667-35.0208-78.22186667-78.22186667s35.0208-78.22293333 78.22186667-78.22293333z\" fill=\"#000000\" ></path></symbol><symbol id=\"baklib\" viewBox=\"0 0 1024 1024\"><path d=\"M769.62929778 269.31048297c-2.66998518-17.58549333-3.05834667-34.97680592-1.45635556-51.90693927 0.92235852 3.14330075 1.45635555 6.85700741 1.43208296 11.22607408 20.46179555 35.11030518 55.62064592 59.77125925 95.87674074 66.79817481 0.03640889 0.07281778 0.08495408 0.14563555 0.12136297 0.21845333 36.45743408 4.28411259 68.36375703 26.78480592 84.79630222 59.35862519a136.50906075 136.50906075 0 0 1-60.68148148 25.15854222c-26.67557925 4.05352297-52.72007111 0.07281778-75.80330666-10.08526222-22.65846518-28.14407111-38.45992297-62.45338075-44.28534519-100.76766814z\" fill=\"#FFA404\" ></path><path d=\"M834.7769363 270.34206815c4.56324741-0.46117925 9.19931259-0.69176889 13.88392295-0.6917689 59.81980445 0 110.65874963 38.42351408 129.21514667 91.92030816 28.69020445-42.18576592 42.16149333-94.88156445 33.89667556-149.30071704-11.4566637-75.41494518-61.74947555-135.28329482-127.4796563-162.42005334-22.24583111 10.36439703-42.02799408 24.68522667-58.66685629 41.88235852-0.78885925 0.83740445-1.57771852 1.66267259-2.35444148 2.51221334-1.26217482 1.34712889-2.46366815 2.73066667-3.68943408 4.10206814a212.83058725 212.83058725 0 0 0-21.17783703 28.79943112c-0.54613333 0.89808592-1.11653925 1.78403555-1.6505363 2.69425777-0.74031408 1.26217482-1.44421925 2.54862222-2.16026075 3.82293333-11.42025482 20.55888592-19.29671111 43.19307852-22.82837333 67.01662816-0.04854518 0.29127111-0.06068148 0.41263408 0.06068149 0.48545184 11.91784297 30.37714963 34.4185363 54.74683259 62.95096889 69.1768889z\" fill=\"#FFA404\" ></path><path d=\"M911.97591703 61.94972445c-41.71245037 85.26961778-65.15977482 181.10994963-65.15977481 282.4358874 0 87.32065185 25.48622222 129.40932741 25.48622223 216.9605689 0 0 0.03640889-0.14563555 0.03640888 1.82044443 0 250.72374518-203.24655408 453.97029925-453.97029925 453.97029927-107.68535703 0-211.37787259-30.93541925-307.76433778-119.3119289 145.64769185-60.32952889 203.92618667-152.00711111 229.55804445-195.49146073-194.24142222-56.61582222-330.77475555-237.37381925-327.78922667-443.6908563 71.60414815 24.72163555 147.26181925 41.42117925 226.05065481 48.75150223 57.12554667 5.32783408 113.52291555 5.46133333 168.77947259 0.9466311C423.47785482 141.30896592 564.27102815 10.74669037 735.59912297 10.74669037c64.46800592 0 124.57908148 18.55639703 175.39375406 50.52340148-0.57040592 0.18204445-1.11653925 0.40049778-1.67480888 0.58254223 0.88594963 0.0121363 1.77189925 0.06068148 2.65784888 0.09709037z\" fill=\"#03C9A9\" ></path><path d=\"M812.1670163 47.21626075c-63.46069333 0-114.90645333 51.42148741-114.90645333 114.85790814s51.44576 114.85790815 114.90645333 114.85790814c13.65333333 0 26.73626075-2.37871408 38.88469333-6.74778073a639.56946489 639.56946489 0 0 1 48.90927407-182.19008c-21.08074667-24.94008889-52.58657185-40.77795555-87.7939674-40.77795555z\" fill=\"#FFFFFF\" ></path><path d=\"M814.01173333 101.90241185c12.07561482 0 21.86960592 9.80612741 21.86960592 21.89387852s-9.79399111 21.89387852-21.86960592 21.89387852-21.86960592-9.80612741-21.86960592-21.89387852 9.79399111-21.89387852 21.86960592-21.89387852z\"  ></path></symbol><symbol id=\"wiznote\" viewBox=\"0 0 1024 1024\"><path d=\"M67.328 189.44h125.44l133.632 402.688L468.224 189.44h87.552l141.824 402.688L831.232 189.44h125.44L742.4 834.56h-87.04L512 426.24l-142.848 409.6H281.6z\" fill=\"#000000\" ></path></symbol><symbol id=\"webdav\" viewBox=\"0 0 1024 1024\"><path d=\"M1024 896H608.000181v-63.999639h351.999458V64.000361H653.248305L525.248305 192.000361H64.000361v640h351.999458v63.999639H0V128h498.751695L626.751695 0h397.248305zM831.999639 960.000361h64.000361v63.999639h-64.000361zM959.999639 960.000361h64.000361v63.999639h-64.000361zM0 960.000361h64.000361v63.999639H0zM128 960.000361h64.000361v63.999639h-64.000361z\"  ></path><path d=\"M256 960.000361h512v63.999639H256z\"  ></path><path d=\"M480.000181 832.000361h63.999638v191.999639h-63.999638z\"  ></path><path d=\"M32.000542 704.000361h959.999639v63.999639H32.000542z\"  ></path></symbol><symbol id=\"dida365\" viewBox=\"0 0 1024 1024\"><path d=\"M526.564641 27.133015v110.702703c-219.415651 5.60749-391.258082 189.931108-383.751281 399.036213 7.054584 198.975446 173.560855 362.858859 372.355414 368.466348 208.472001 5.87882 390.444091-164.516517 397.136902-381.851969H1023.008379c-4.522169 283.540011-241.212507 508.201378-513.627981 500.24236-260.567391-7.597244-476.003533-226.108461-481.158806-486.856739C22.794989 265.722664 245.918819 32.469175 526.564641 27.133015z\"  ></path><path d=\"M381.493452 399.48843l-84.021904 98.945062 189.026674 158.45681a66.928105 66.928105 0 0 0 47.754107 20.982865 66.204557 66.204557 0 0 0 43.955484-17.184243l357.070483-446.79032-106.904081-80.223282L524.66533 519.778131z\"  ></path><path d=\"M499.431626 0v110.702703c-219.415651 5.60749-391.258082 189.931108-383.751281 399.036212 7.054584 198.975446 173.560855 362.858859 372.355414 368.466349 208.472001 5.87882 390.444091-164.516517 397.136902-381.85197H995.875363c-4.522169 283.540011-241.212507 508.201378-513.627981 500.24236-260.567391-7.597244-476.003533-226.108461-481.158805-486.856739C-4.338026 238.589648 218.785803 5.33616 499.431626 0z\" fill=\"#617FDE\" ></path><path d=\"M354.360437 372.355414l-84.021905 98.945063 189.026674 158.45681A66.928105 66.928105 0 0 0 507.481087 651.101925a66.204557 66.204557 0 0 0 43.955485-17.184243l357.070482-446.790319-106.904081-80.223282L497.532314 492.645116z\" fill=\"#FFB000\" ></path></symbol><symbol id=\"confluence\" viewBox=\"0 0 1024 1024\"><path d=\"M908.66688 905.073778C826.078436 988.344889 704.649102 1024 512.293547 1024c-189.127111 0-313.770667-35.655111-396.344889-118.926222-82.588444-83.271111-121.358222-217.6-115.342222-396.999111-4.579556-189.511111 32.753778-313.742222 115.342222-396.999111C198.537102 27.804444 327.475769 0 512.009102 0c181.248 0 314.069333 27.804444 396.657778 111.075556C991.255324 194.346667 1024.009102 325.361778 1024.009102 508.074667c0 178.801778-32.753778 313.728-115.342222 396.999111z\" fill=\"#5CB4FF\" ></path><path d=\"M235.13088 668.245333c-6.215111 10.140444-13.198222 21.902222-19.114667 31.260445a19.114667 19.114667 0 0 0 6.4 25.998222l124.273778 76.472889a19.114667 19.114667 0 0 0 26.467556-6.499556c4.977778-8.32 11.377778-19.114667 18.346666-30.677333 49.237333-81.265778 98.759111-71.324444 188.046223-28.686222l123.207111 58.595555a19.114667 19.114667 0 0 0 25.728-9.557333l59.164444-133.831111a19.114667 19.114667 0 0 0-9.557333-25.031111 15014.968889 15014.968889 0 0 1-124.273778-59.079111c-167.480889-81.351111-309.816889-76.088889-418.702222 101.034666z\" fill=\"#DBEFFF\" ></path><path d=\"M792.329102 364.544c6.215111-10.126222 13.198222-21.888 19.128889-31.246222a19.114667 19.114667 0 0 0-6.4-26.012445l-124.273778-76.472889a19.114667 19.114667 0 0 0-27.249777 6.314667c-4.977778 8.32-11.377778 19.114667-18.346667 30.677333-49.237333 81.265778-98.759111 71.324444-188.032 28.686223l-122.837333-58.311111a19.114667 19.114667 0 0 0-25.713778 9.557333l-59.164445 133.831111a19.114667 19.114667 0 0 0 9.543111 25.031111c26.012444 12.245333 77.724444 36.622222 124.273778 59.093333 167.864889 81.251556 310.186667 75.804444 419.072-101.148444z\" fill=\"#9ED2FF\" ></path></symbol><symbol id=\"medium\" viewBox=\"0 0 1024 1024\"><path d=\"M512 1024C229.21216 1024 0 794.78784 0 512S229.21216 0 512 0 1024 229.21216 1024 512 794.78784 1024 512 1024z m274.432-353.15712h-21.42208c-4.01408 0-8.192-2.048-12.53376-6.144-4.3008-4.13696-6.47168-8.11008-6.47168-11.91936V372.81792c0-3.76832 2.21184-8.02816 6.63552-12.6976 4.42368-4.66944 8.56064-6.9632 12.36992-6.9632h21.42208V286.72h-202.1376l-67.25632 260.87424h-1.8432L448.512 286.72H245.76v66.43712h20.80768c4.21888 0 8.6016 2.33472 12.98432 6.9632 4.42368 4.66944 6.63552 8.88832 6.63552 12.6976v279.9616c0 3.80928-2.21184 7.7824-6.63552 11.8784-4.42368 4.13696-8.76544 6.18496-12.98432 6.18496H245.76V737.28h162.32448v-66.43712h-40.7552V376.66816h2.37568L463.2576 737.28h73.3184l94.74048-360.61184h1.80224v294.17472h-40.42752V737.28h193.7408v-66.43712z\" fill=\"#666666\" ></path></symbol><symbol id=\"ulysses\" viewBox=\"0 0 1024 1024\"><path d=\"M463.4 471.3c0-114-169.3-407.3-334.5-407.3-82.6 0 24.8 162.9 41.3 285.1 4.1 40.7-33 162.9-33 203.6s24.8 81.5 148.7 81.5c144.6 0 161.1-81.5 161.1-81.5s16.4-20.3 16.4-81.4zM562.6 471.3c0-114 169.3-407.3 334.5-407.3 82.6 0-24.8 162.9-41.3 285.1-4.1 40.7 33 162.9 33 203.6s-24.8 81.5-148.7 81.5c-144.6 0-161.1-81.5-161.1-81.5s-16.4-20.3-16.4-81.4zM471.7 674.9s-198.3-16.3-289.1 73.3C100 829.7 137.1 960 306.5 960c185.9 0 165.2-195.5 165.2-195.5v-89.6zM554.3 674.9s198.3-16.3 289.1 73.3C926 829.7 888.9 960 719.5 960c-185.9 0-165.2-195.5-165.2-195.5v-89.6z\"  ></path></symbol><symbol id=\"coffee\" viewBox=\"0 0 1027 1024\"><path d=\"M559.854933 278.493867a23.210667 23.210667 0 0 0 23.210667-23.210667v-139.264a23.210667 23.210667 0 1 0-46.421333 0v139.264c0 12.834133 10.376533 23.210667 23.210666 23.210667z m-139.264 0a23.210667 23.210667 0 0 0 23.210667-23.210667v-232.106667a23.210667 23.210667 0 1 0-46.421333 0v232.106667c0 12.834133 10.410667 23.210667 23.210666 23.210667z m278.493867 46.421333a23.210667 23.210667 0 0 0 23.210667-23.210667v-232.106666a23.210667 23.210667 0 1 0-46.421334 0v232.106666c0 12.8 10.410667 23.210667 23.210667 23.210667zM142.1312 278.459733a23.210667 23.210667 0 0 0 23.210667-23.210666v-139.264a23.210667 23.210667 0 1 0-46.421334 0v139.264c0 12.834133 10.376533 23.210667 23.210667 23.210666z m742.6048 208.861867h-46.421333v-69.632c0-25.6-20.7872-46.421333-46.421334-46.421333H49.288533c-25.6 0-46.421333 20.821333-46.421333 46.421333v278.493867c0 118.3744 63.556267 221.696 158.208 278.459733H26.077867a23.210667 23.210667 0 1 0 0 46.421333H815.104a23.210667 23.210667 0 1 0 0-46.421333h-134.792533a325.256533 325.256533 0 0 0 150.357333-208.861867h54.0672a139.264 139.264 0 1 0 0-278.459733zM513.4336 974.677333h-185.685333a277.504 277.504 0 0 1-207.121067-92.842666H720.554667a277.504 277.504 0 0 1-207.189334 92.842666z m278.493867-278.459733a276.7872 276.7872 0 0 1-37.5808 139.229867H86.869333a276.7872 276.7872 0 0 1-37.5808-139.264v-278.459734h742.638934v278.493867z m92.842666 23.210667h-47.616c0.546133-7.714133 1.160533-15.36 1.160534-23.210667v-162.474667h46.421333a92.842667 92.842667 0 0 1 0 185.685334zM281.326933 324.881067a23.210667 23.210667 0 0 0 23.210667-23.210667V69.632a23.210667 23.210667 0 1 0-46.421333 0v232.072533c0 12.8 10.376533 23.210667 23.210666 23.210667z\" fill=\"#333333\" ></path></symbol><symbol id=\"jianguo\" viewBox=\"0 0 1024 1024\"><path d=\"M873.728 322.56c0 0-1.6-2.24-2.496-3.072-10.624-10.624-28.224-10.24-38.912 0.448-8.96 8.896-9.792 24.96-4.224 32.896 0.512 0.832 0.768 1.408 1.024 1.664 73.216 117.632 94.656 262.528 55.232 399.168l-0.64 2.176L883.2 758.144c-1.984 10.304-7.04 19.712-14.464 27.2-10.112 9.856-23.296 15.424-37.568 15.424-14.08 0-27.456-5.568-37.504-15.424-2.304-2.304-4.288-4.864-6.464-7.936l-8.192-12.032c47.808-86.72 32.512-306.88-89.28-428.48C598.848 246.016 494.976 221.824 403.456 221.824c-3.264 0-11.392 0.064-12.224 0.128-0.064 0-0.128 0-0.192 0l0 0c-6.4 0.384-12.672 2.88-17.536 7.808-10.688 10.688-10.688 27.968 0 38.592 5.312 5.312 12.288 8 19.328 8 2.944-0.064 7.744 0 10.688 0 99.968 0 180.992 32.384 247.68 99.072 50.88 50.752 85.76 130.112 95.68 217.728 9.408 82.112-7.04 139.712-21.76 154.432-95.04 94.848-241.088 158.72-363.264 158.72-73.984 0-133.76-22.208-177.664-65.984-54.976-54.912-65.088-131.392-63.808-185.856C123.072 535.68 181.888 398.016 266.112 308.032c0 0 0-0.064 0.128-0.064 19.52-21.44 33.6-37.12 33.6-37.12S272.192 254.016 255.232 243.52l0.256-0.064c-0.064 0-0.128 0.064-0.256 0.064l-9.216-6.4C243.136 235.136 240.704 233.152 238.656 231.104c-20.608-20.608-20.608-54.208 0-74.816 5.44-5.44 12.032-9.6 19.52-12.224l17.216-6.208c42.816-11.712 87.04-17.664 131.776-17.664 132.608 0 257.408 51.584 351.232 145.216 7.68 7.68 14.912 15.616 22.016 23.68l25.792-25.664 0 0 10.624-10.56 2.368-2.496 0 0 71.872-71.744c12.352-12.288 12.352-32.32 0-44.672-12.48-12.288-32.512-12.288-44.8 0l-71.808 71.616c-104.384-93.312-235.84-140.032-367.296-140.032-56.448 0-112.832 8.64-167.104 25.792-0.192 0.384-0.256 0.832-0.448 1.28C225.216 97.856 211.584 106.112 200 117.632c-42.048 41.984-42.048 110.08 0 152.064C204.608 274.368 209.6 278.4 214.784 281.92c0 0.512 0 0.896 0 1.344-143.232 163.392-215.104 450.176-69.312 595.712 57.728 57.664 134.464 82.048 216.32 82.048 132.096 0 277.504-63.488 378.688-153.088 0.576 0 1.088 0 1.536 0.064 3.776 5.632 8.064 10.944 12.928 15.872 21.056 20.928 48.64 31.488 76.16 31.488s55.168-10.56 76.16-31.488c15.744-15.68 25.536-34.88 29.504-55.168C979.776 619.968 958.848 456.96 873.728 322.56z\"  ></path></symbol><symbol id=\"joplin\" viewBox=\"0 0 1024 1024\"><path d=\"M513.8176 140.032c-4.96896 5.66784-4.97664 5.74976-4.41344 49.33888 0.69632 53.7856-1.77664 49.14176 26.18112 49.18784 29.54752 0.04864 35.59936 2.46016 42.4192 16.90112l3.39968 7.19616 0.55808 210.75712c0.33536 127.19616 0.18176 213.06368-0.38912 216.576-1.39776 8.576-7.41632 26.08896-10.26304 29.8624-3.3792 4.47744-16.25344 17.42848-17.32608 17.42848-0.47616 0-2.33472 1.23648-4.13184 2.74688-2.92352 2.46016-8.64768 4.56192-25.05216 9.19808-7.05536 1.99424-31.8976 1.91488-39.424-0.12544-3.0976-0.83968-9.7792-2.51904-14.848-3.73504-8.47104-2.03008-29.4144-11.09504-30.7968-13.32992-0.32256-0.51968-3.28192-2.30656-6.5792-3.97056-3.29728-1.664-6.2976-3.51488-6.66624-4.11392-0.3712-0.59904-2.36288-2.17088-4.4288-3.49184-3.77088-2.41408-18.27328-16.50176-24.50176-23.80544-15.56224-18.24-27.27424-47.7952-27.32544-68.9408-0.02816-11.33824 5.3888-31.68256 9.29536-34.92608 0.85504-0.70912 2.39616-2.944 3.42784-4.9664 1.74848-3.42528 8.2688-9.93024 15.0528-15.01696 17.10848-12.82816 57.8432-18.06848 79.05792-10.16832 21.01248 7.82592 24.08448 8.7168 27.91424 8.10496 6.21312-0.99584 6.16448-0.41472 5.80608-70.88384l-0.32-62.83264-3.584-2.99264c-10.27328-8.576-71.93088-13.42208-99.328-7.81056-22.1056 4.5312-35.94496 8.01024-39.52384 9.93792-2.19904 1.18528-5.83936 2.63168-8.09216 3.21536-3.73248 0.96768-13.9264 5.48864-23.04 10.21952-12.51072 6.49216-17.2544 9.20064-17.56672 10.03264-0.19456 0.51968-4.50048 3.74016-9.56928 7.15776-9.20064 6.20032-30.10304 25.6896-35.8656 33.44128-1.6768 2.2528-4.8896 6.4768-7.1424 9.38496-7.70816 9.95584-10.48064 14.1696-11.12832 16.90368-0.35584 1.50528-1.81248 4.12416-3.2384 5.81632-1.42336 1.69216-3.48928 5.69344-4.58752 8.88576-1.1008 3.19488-2.9824 7.3984-4.18304 9.34144-2.0736 3.35616-6.53312 16.768-9.64864 29.02784-6.11328 24.04608-4.93568 94.70976 1.7152 103.06048 0.51456 0.64512 2.0352 5.61408 3.3792 11.0464 1.344 5.42976 3.39712 11.48416 4.55936 13.45792 1.1648 1.9712 2.97984 6.21568 4.03712 9.4336 1.05728 3.21792 3.3664 8.13056 5.1328 10.91584s3.21024 5.82144 3.21024 6.75072c0 0.92672 1.92256 4.63872 4.27008 8.24576 2.35008 3.6096 6.48448 10.01728 9.18784 14.24128 11.46368 17.89952 33.87648 43.17184 50.40384 56.832 2.7264 2.2528 6.17472 5.2864 7.6672 6.74304 1.48992 1.45664 4.43904 3.68384 6.55104 4.94848 2.112 1.26464 3.84 2.6368 3.84 3.05152 0 0.41472 1.9584 1.85088 4.352 3.19488 2.3936 1.344 6.34624 3.96288 8.78336 5.82144 2.43456 1.856 5.0176 3.37664 5.73696 3.37664 0.71936 0 2.51904 1.12128 3.99616 2.49088 2.61888 2.42688 24.75264 13.89312 26.81856 13.89312 0.57088 0 2.64192 1.08288 4.60288 2.40384 1.96096 1.32352 6.79168 3.46112 10.73408 4.75136 3.9424 1.29024 9.24416 3.38432 11.77856 4.65152 2.53696 1.26976 8.75776 3.25888 13.824 4.42112a405.1456 405.1456 0 0 1 16.38144 4.14976c30.49728 8.66816 99.04384 8.19456 123.55328-0.84992 3.008-1.11104 8.69632-2.79552 12.63872-3.74272 3.9424-0.94976 9.7024-3.03616 12.8-4.63872 3.0976-1.60256 7.69792-3.63008 10.22208-4.50816 2.52672-0.87808 5.75232-2.36544 7.168-3.3024 1.41824-0.93952 4.88192-2.94144 7.69792-4.44672 14.82752-7.93088 19.02848-10.432 20.20864-12.02176 0.71168-0.96256 2.67776-2.61888 4.36736-3.68128 8.3584-5.25312 33.14688-29.38624 37.75744-36.75904 0.99072-1.58464 2.48832-3.65824 3.32288-4.608 2.432-2.75456 8.24064-11.55072 11.06176-16.74752a123.6992 123.6992 0 0 1 5.17888-8.59136c1.4208-2.09408 2.58304-4.57472 2.58304-5.5168 0-0.93952 1.6128-4.98176 3.58144-8.98048s4.3136-10.1248 5.20704-13.61664a149.20192 149.20192 0 0 1 4.03712-12.75392c4.71552-12.52864 4.68992-11.53536 5.64736-204.31616 0.50432-101.0944 1.04448-202.7008 1.20576-225.792l0.28928-41.984 2.91584-5.84704c6.69952-13.42976 13.68832-16.15616 41.536-16.20224 29.76-0.04864 27.73248 3.62496 27.7376-50.24512l0.00256-40.5504-2.9568-4.08064-2.9568-4.0832-32.5376-0.6784c-76.0448-1.58976-241.67424-0.832-243.3792 1.11104\" fill=\"#F8FAFB\" ></path><path d=\"M196.608 2.048c0 1.94816-0.68352 2.048-13.89056 2.048-15.35488 0-17.29536 0.54272-18.20416 5.08416L163.8912 12.288H148.48V20.48h-16.384v6.9376l-7.22688 0.40704c-7.54176 0.42752-9.11104 1.37984-9.14176 5.55008-0.0128 1.8688-0.90112 2.26048-6.92736 3.072-6.59456 0.88576-6.95552 1.08032-7.87712 4.20608-0.82432 2.79808-1.50272 3.32288-4.608 3.584-3.39456 0.28672-3.66336 0.56576-3.95776 4.08832-0.2944 3.55328-0.54272 3.8016-4.096 4.096-3.55328 0.29696-3.79904 0.54272-4.096 4.096-0.2944 3.55328-0.54272 3.8016-4.096 4.096-3.44064 0.28672-3.80928 0.6144-4.10112 3.64288-0.26368 2.7264-0.76032 3.328-2.74432 3.328a5.6576 5.6576 0 0 0-5.63968 5.63968c0 1.984-0.6016 2.48064-3.328 2.74432-3.02848 0.29184-3.35616 0.66048-3.64288 4.10112-0.2944 3.55328-0.54272 3.8016-4.096 4.096-3.55328 0.29696-3.79904 0.54272-4.096 4.096-0.2944 3.55328-0.54272 3.8016-4.096 4.096-3.52256 0.2944-3.8016 0.5632-4.08832 3.95776-0.26112 3.10528-0.78592 3.78368-3.584 4.608-3.12576 0.9216-3.32032 1.28256-4.20608 7.87712-0.81152 6.02624-1.2032 6.91456-3.072 6.92736-3.72224 0.02816-4.9664 1.65632-5.49888 7.18848C27.34848 128.40192 24.99072 132.096 22.016 132.096c-1.1776 0-1.536 1.30304-1.536 5.58592C20.48 143.4624 17.38496 148.48 13.82144 148.48c-1.09568 0-1.52064 1.92512-1.74592 7.936-0.28672 7.60064-0.40704 7.9488-2.85952 8.26368-4.42624 0.56832-5.12 2.69824-5.12 15.73888 0 11.40992-0.1152 12.09344-2.048 12.09344-2.04288 0-2.048 0.68352-2.048 319.488s0.00512 319.488 2.048 319.488c1.9328 0 2.048 0.68352 2.048 12.09344 0 13.04064 0.69376 15.17056 5.12 15.73888 2.45248 0.31488 2.5728 0.66304 2.85952 8.26368 0.22528 6.01088 0.65024 7.936 1.74592 7.936C17.38496 875.52 20.48 880.5376 20.48 886.31808c0 4.28288 0.3584 5.58592 1.536 5.58592 2.92096 0 5.2992 3.6864 5.95456 9.23392 0.67072 5.66272 1.7664 7.10656 5.40416 7.13472 1.8688 0.0128 2.26048 0.90112 3.072 6.92736 0.88576 6.59456 1.08032 6.95552 4.20608 7.87712 2.79808 0.82432 3.32288 1.50272 3.584 4.608 0.28672 3.39456 0.56576 3.66336 4.08832 3.95776 3.55328 0.2944 3.8016 0.54272 4.096 4.096 0.29696 3.55328 0.54272 3.79904 4.096 4.096 3.55328 0.2944 3.8016 0.54272 4.096 4.096 0.28672 3.44064 0.6144 3.80928 3.64288 4.10112 2.53952 0.24576 3.328 0.8064 3.328 2.37312 0 3.70688 1.95072 6.01088 5.09184 6.01088 2.60352 0 3.01056 0.41216 3.29216 3.328 0.29184 3.02848 0.66048 3.35616 4.10112 3.64288 3.55328 0.2944 3.8016 0.54272 4.096 4.096 0.29696 3.55328 0.54272 3.79904 4.096 4.096 3.55328 0.2944 3.8016 0.54272 4.096 4.096 0.2944 3.52256 0.5632 3.8016 3.95776 4.08832 3.10528 0.26112 3.78368 0.78592 4.608 3.584 0.9216 3.12576 1.28256 3.32032 7.87712 4.20608 6.23104 0.83712 6.912 1.15968 6.912 3.25376 0 4.07808 2.59584 5.54496 9.80224 5.54496H132.096v7.168H148.48v8.192h15.4112l0.62208 3.10784c0.9088 4.54144 2.84928 5.08416 18.20416 5.08416 13.20704 0 13.89056 0.09984 13.89056 2.048 0 2.04288 0.68352 2.048 315.904 2.048 315.22048 0 315.904-0.00512 315.904-2.048 0-1.94304 0.68352-2.048 13.37856-2.048 14.83264 0 16.78848-0.56064 17.69216-5.08416l0.62208-3.10784H875.52V1003.52h16.384v-7.168h6.58176c7.21408 0 9.80224-1.46688 9.80224-5.56032 0-2.11712 0.60928-2.40384 6.4-3.00032 6.55616-0.6784 8.96-2.26304 8.96-5.90848 0-1.3312 0.98816-1.87392 3.84-2.10944 3.61984-0.29952 3.85792-0.53504 4.15488-4.09856 0.2944-3.55328 0.54272-3.8016 4.096-4.096 3.55328-0.29696 3.79904-0.54272 4.096-4.096 0.2944-3.55328 0.54272-3.8016 4.096-4.096 3.42528-0.28416 3.81184-0.62208 4.09856-3.584 0.28672-2.96192 0.67328-3.29984 4.096-3.584 3.54816-0.29696 3.79648-0.54528 4.09344-4.09344 0.28416-3.42272 0.62208-3.80928 3.584-4.096 2.96192-0.28672 3.29984-0.67328 3.584-4.09856 0.2944-3.55328 0.54272-3.8016 4.096-4.096 3.55328-0.29696 3.79904-0.54272 4.096-4.096 0.2944-3.55328 0.54272-3.8016 4.096-4.096 3.56352-0.29696 3.79904-0.53504 4.09856-4.15488 0.23552-2.85184 0.77824-3.84 2.10944-3.84 3.64544 0 5.23008-2.40384 5.90848-8.96 0.59648-5.79072 0.8832-6.4 3.00032-6.4 3.64288 0 5.56032-2.67264 5.56032-7.75424 0-4.736 2.54208-8.62976 5.632-8.62976 1.1776 0 1.536-1.30304 1.536-5.58592 0-5.78048 3.09504-10.79808 6.65856-10.79808 1.09568 0 1.52064-1.92512 1.74592-7.936 0.28672-7.60064 0.40704-7.9488 2.85952-8.26368 4.42624-0.56832 5.12-2.69824 5.12-15.73888 0-11.40992 0.1152-12.09344 2.048-12.09344 2.04288 0 2.048-0.68352 2.048-319.488s-0.00512-319.488-2.048-319.488c-1.9328 0-2.048-0.68352-2.048-12.09344 0-13.04064-0.69376-15.17056-5.12-15.73888-2.45248-0.31488-2.5728-0.66304-2.85952-8.26368-0.22528-6.01088-0.65024-7.936-1.74592-7.936-3.56352 0-6.65856-5.0176-6.65856-10.79808 0-4.28288-0.3584-5.58592-1.536-5.58592-3.08992 0-5.632-3.89376-5.632-8.62976 0-5.07392-1.91488-7.75424-5.54496-7.75424-2.09408 0-2.41664-0.68096-3.25376-6.912-0.88576-6.59456-1.08032-6.95552-4.20608-7.87712-2.79808-0.82432-3.32288-1.50272-3.584-4.608-0.28672-3.39456-0.56576-3.66336-4.08832-3.95776-3.55328-0.2944-3.8016-0.54272-4.096-4.096-0.29696-3.55328-0.54272-3.79904-4.096-4.096-3.55328-0.2944-3.8016-0.54272-4.096-4.096-0.28672-3.44064-0.6144-3.80928-3.64288-4.10112-2.91584-0.2816-3.328-0.68864-3.328-3.29216 0-3.14112-2.304-5.09184-6.01088-5.09184-1.56672 0-2.12736-0.78848-2.37312-3.328-0.29184-3.02848-0.66048-3.35616-4.10112-3.64288-3.55328-0.2944-3.8016-0.54272-4.096-4.096-0.29696-3.55328-0.54272-3.79904-4.096-4.096-3.55328-0.2944-3.8016-0.54272-4.096-4.096-0.2944-3.52256-0.5632-3.8016-3.95776-4.08832-3.10528-0.26112-3.78368-0.78592-4.608-3.584-0.9216-3.12576-1.28256-3.32032-7.87712-4.20608-6.02624-0.81152-6.91456-1.2032-6.92736-3.072-0.03072-4.17024-1.6-5.12256-9.14176-5.55008l-7.22688-0.40704V20.48H875.52V12.288h-7.56992c-7.87968 0-8.81408-0.5376-8.81408-5.08416 0-2.59584-2.74688-3.07712-17.664-3.09504-12.36992-0.0128-13.056-0.12032-13.056-2.0608 0-2.04288-0.68352-2.048-315.904-2.048C197.29152 0 196.608 0.00512 196.608 2.048m560.5888 136.87296l32.5376 0.6784 2.9568 4.0832 2.9568 4.08064-0.00256 40.5504c-0.00512 53.87008 2.0224 50.19648-27.7376 50.24512-27.84768 0.04608-34.83648 2.77248-41.536 16.20224L723.456 260.608l-0.28928 41.984c-0.16128 23.0912-0.70144 124.6976-1.20576 225.792-0.95744 192.7808-0.93184 191.78752-5.64736 204.31616a149.20192 149.20192 0 0 0-4.03712 12.75392c-0.89344 3.49184-3.2384 9.61792-5.20704 13.61664-1.96864 3.99872-3.58144 8.04096-3.58144 8.98048 0 0.94208-1.16224 3.42272-2.58304 5.5168a123.6992 123.6992 0 0 0-5.17888 8.59136c-2.82112 5.1968-8.62976 13.99296-11.06176 16.74752-0.83456 0.94976-2.33216 3.02336-3.32288 4.608-4.61056 7.3728-29.39904 31.50592-37.75744 36.75904-1.6896 1.0624-3.65568 2.71872-4.36736 3.68128-1.18016 1.58976-5.38112 4.09088-20.20864 12.02176-2.816 1.50528-6.27968 3.5072-7.69792 4.44672-1.41568 0.93696-4.64128 2.42432-7.168 3.3024-2.52416 0.87808-7.12448 2.9056-10.22208 4.50816-3.0976 1.60256-8.8576 3.68896-12.8 4.63872-3.9424 0.9472-9.63072 2.63168-12.63872 3.74272-24.50944 9.04448-93.056 9.51808-123.55328 0.84992a405.1456 405.1456 0 0 0-16.38144-4.14976c-5.06624-1.16224-11.28704-3.15136-13.824-4.42112-2.5344-1.2672-7.83616-3.36128-11.77856-4.65152s-8.77312-3.42784-10.73408-4.75136c-1.96096-1.32096-4.032-2.40384-4.60288-2.40384-2.06592 0-24.19968-11.46624-26.81856-13.89312-1.47712-1.3696-3.2768-2.49088-3.99616-2.49088-0.71936 0-3.3024-1.52064-5.73696-3.37664-2.43712-1.85856-6.38976-4.47744-8.78336-5.82144-2.3936-1.344-4.352-2.78016-4.352-3.19488 0-0.41472-1.728-1.78688-3.84-3.05152s-5.06112-3.49184-6.55104-4.94848c-1.49248-1.45664-4.9408-4.49024-7.6672-6.74304-16.52736-13.66016-38.94016-38.93248-50.40384-56.832-2.70336-4.224-6.83776-10.63168-9.18784-14.24128-2.34752-3.60704-4.27008-7.31904-4.27008-8.24576 0-0.92928-1.44384-3.96544-3.21024-6.75072-1.7664-2.78528-4.07552-7.69792-5.1328-10.91584-1.05728-3.21792-2.87232-7.4624-4.03712-9.4336-1.16224-1.97376-3.21536-8.02816-4.55936-13.45792-1.344-5.43232-2.86464-10.40128-3.3792-11.0464-6.65088-8.35072-7.82848-79.0144-1.7152-103.06048 3.11552-12.25984 7.57504-25.67168 9.64864-29.02784 1.20064-1.94304 3.08224-6.14656 4.18304-9.34144 1.09824-3.19232 3.16416-7.1936 4.58752-8.88576 1.42592-1.69216 2.88256-4.31104 3.2384-5.81632 0.64768-2.73408 3.42016-6.94784 11.12832-16.90368 2.2528-2.90816 5.4656-7.13216 7.1424-9.38496 5.76256-7.75168 26.66496-27.24096 35.8656-33.44128 5.0688-3.4176 9.37472-6.63808 9.56928-7.15776 0.31232-0.832 5.056-3.54048 17.56672-10.03264 9.1136-4.73088 19.30752-9.25184 23.04-10.21952 2.2528-0.58368 5.89312-2.03008 8.09216-3.21536 3.57888-1.92768 17.41824-5.40672 39.52384-9.93792 27.39712-5.61152 89.05472-0.76544 99.328 7.81056l3.584 2.99264 0.32 62.83264c0.3584 70.46912 0.40704 69.888-5.80608 70.88384-3.82976 0.61184-6.90176-0.27904-27.91424-8.10496-21.21472-7.90016-61.94944-2.65984-79.05792 10.16832-6.784 5.08672-13.30432 11.59168-15.0528 15.01696-1.03168 2.0224-2.5728 4.25728-3.42784 4.9664-3.90656 3.24352-9.32352 23.58784-9.29536 34.92608 0.0512 21.1456 11.7632 50.7008 27.32544 68.9408 6.22848 7.30368 20.73088 21.39136 24.50176 23.80544 2.06592 1.32096 4.0576 2.8928 4.4288 3.49184 0.36864 0.59904 3.36896 2.44992 6.66624 4.11392s6.25664 3.45088 6.5792 3.97056c1.3824 2.23488 22.32576 11.29984 30.7968 13.32992 5.0688 1.216 11.7504 2.89536 14.848 3.73504 7.5264 2.04032 32.36864 2.11968 39.424 0.12544 16.40448-4.63616 22.12864-6.73792 25.05216-9.19808 1.79712-1.5104 3.65568-2.74688 4.13184-2.74688 1.07264 0 13.94688-12.95104 17.32608-17.42848 2.84672-3.77344 8.86528-21.2864 10.26304-29.8624 0.57088-3.51232 0.72448-89.37984 0.38912-216.576l-0.55808-210.75712-3.39968-7.19616c-6.81984-14.44096-12.87168-16.85248-42.4192-16.90112-27.95776-0.04608-25.4848 4.59776-26.18112-49.18784-0.5632-43.58912-0.55552-43.67104 4.41344-49.33888 1.70496-1.94304 167.3344-2.7008 243.3792-1.11104\" fill=\"#0C69CC\" ></path></symbol><symbol id=\"qq_docs\" viewBox=\"0 0 1181 1024\"><path d=\"M944.643052 0H194.788816a21.266874 21.266874 0 0 0-20.95181 17.564863L0.315065 998.952348a21.266874 21.266874 0 0 0 20.991192 24.929503h413.365025l32.76674-10.121457h191.953232l26.819892 10.121457h318.412368a21.306257 21.306257 0 0 0 20.991193-17.564863l142.724357-807.196031L944.643052 0z\" fill=\"#0188FB\" ></path><path d=\"M934.83666 199.120957h233.502404L944.643052 0l-30.797585 174.152071a21.266874 21.266874 0 0 0 20.991193 24.968886\" fill=\"#00DCFF\" ></path><path d=\"M608.429522 42.021768L566.447137 280.013846l-409.032884 0.708895 177.735934 191.795701h197.230568l-97.67009 551.363409h251.500481l97.827622-551.363409h296.987962l10.318372-0.236299z\" fill=\"#FFFFFF\" ></path></symbol><symbol id=\"wechat\" viewBox=\"0 0 1024 1024\"><path d=\"M332.820602 413.878041c-12.798544 4.266181-29.86327-4.266181-34.129451-21.330907-4.266181-17.064726 4.266181-34.129451 21.330906-34.129451h12.798545c17.064726 0 29.86327 12.798544 29.863269 29.86327 0 12.798544-12.798544 25.597088-29.863269 25.597088z m174.913437 0c-17.064726 4.266181-34.129451-4.266181-38.395633-21.330907-4.266181-17.064726 4.266181-34.129451 21.330907-38.395632h12.798544c17.064726 0 29.86327 12.798544 29.86327 29.86327 4.266181 17.064726-8.532363 29.86327-25.597088 29.863269z m81.057446 162.114893c-12.798544 0-25.597088-8.532363-25.597088-21.330907s8.532363-25.597088 21.330907-25.597088c12.798544-4.266181 25.597088 4.266181 29.863269 17.064725 4.266181 12.798544-4.266181 25.597088-17.064725 29.86327h-8.532363z m136.517804 0c-12.798544 0-25.597088-8.532363-25.597088-21.330907s8.532363-25.597088 21.330907-25.597088c12.798544-4.266181 25.597088 4.266181 29.86327 17.064725 4.266181 12.798544-4.266181 25.597088-17.064726 29.86327H725.309289z m-68.258902-140.783986c-119.453079 0-209.042888 76.791265-209.042888 170.647256s93.855991 170.647256 209.042888 170.647255c25.597088 0 51.194177-4.266181 72.525084-12.798544l68.258902 34.129451-17.064726-59.726539c46.927995-29.86327 81.057446-81.057446 85.323628-136.517805 0-89.589809-98.122172-166.381074-209.042888-166.381074zM413.878048 256.02933c-136.517804 0-247.438521 89.589809-247.438521 200.510525 4.266181 68.258902 38.395632 127.985442 98.122172 157.848712l-25.597088 72.525083 85.323628-42.661814c29.86327 8.532363 55.460358 12.798544 85.323628 12.798545h21.330907c-4.266181-17.064726-8.532363-34.129451-8.532363-51.194177 4.266181-110.920716 98.122172-191.978162 209.042888-187.711981h25.597088C644.251843 328.554414 537.597308 256.02933 413.878048 256.02933z m102.388353 767.91265C234.69843 1028.208161 4.324635 797.834366 0.058453 516.266395-4.207728 234.698423 226.166067 4.324628 507.734039 0.058447s511.941767 226.107614 516.207948 507.675585v4.266181c0 281.567972-226.107614 511.941767-507.675586 511.941767z\" fill=\"#67CC79\" ></path></symbol><symbol id=\"ocr\" viewBox=\"0 0 1024 1024\"><path d=\"M960.553 96.837v259.378h43.211V96.837c0-47.769-38.698-86.468-86.468-86.468H679.521v43.209h237.821c23.874 0.025 43.211 19.386 43.211 43.259z m-907.908 0c0-11.485 4.536-22.482 12.656-30.579a43.152 43.152 0 0 1 30.553-12.68h237.969V10.369H95.854c-47.745 0-86.467 38.699-86.467 86.468v259.378h43.258V96.837z m0 821.442V658.852H9.583V918.28c0 47.745 38.722 86.467 86.468 86.467l237.773 0.463v-43.722H95.854c-23.873-0.001-43.209-19.337-43.209-43.209z m951.166 0V658.852h-43.258V918.28c0 23.799-19.264 43.136-43.062 43.208H679.717v44.429l240.237 0.463a85.83 85.83 0 0 0 60.229-26.627 85.866 85.866 0 0 0 23.628-61.474z m-270.278-632.17H259.109v49.939h224.728v474.426h49.94V336.048h199.757v-49.939z\"  ></path></symbol><symbol id=\"pangu\" viewBox=\"0 0 1024 1024\"><path d=\"M64 635.2V388.8h82v165.8h731.9V388.8H960v246.4H64z m0 0\"  ></path></symbol><symbol id=\"kindle\" viewBox=\"0 0 1024 1024\"><path d=\"M123.6385189 924.63407445V99.36592555c0-53.39970333 43.69066667-97.09036999 97.09036999-97.09037h582.54222222c53.39970333 0 97.09036999 43.69066667 97.09036999 97.09037v825.2681489c0 53.39970333-43.69066667 97.09036999-97.09036999 97.09037H220.72888889c-53.39970333 0-97.09036999-43.69066667-97.09036999-97.09037z\" fill=\"#37474F\" ></path><path d=\"M778.9985189 75.09333333H245.0014811c-14.56355555 0-24.27259221 9.70903666-24.27259221 24.27259222v703.90518556c0 14.56355555 9.70903666 24.27259221 24.27259221 24.27259222h533.9970378c14.56355555 0 24.27259221-9.70903666 24.27259221-24.27259222V99.36592555c0-14.56355555-9.70903666-24.27259221-24.27259221-24.27259222z\" fill=\"#EEEEEE\" ></path><path d=\"M426.666667 853.333333h170.666666v42.666667h-170.666666z\" fill=\"#546E7A\" ></path><path d=\"M341.333333 234.666667h341.333334v64H341.333333zM341.333333 384h341.333334v42.666667H341.333333zM341.333333 469.333333h256v42.666667H341.333333zM341.333333 554.666667h341.333334v42.666666H341.333333zM341.333333 640h256v42.666667H341.333333z\" fill=\"#A1A1A1\" ></path></symbol><symbol id=\"auto\" viewBox=\"0 0 1024 1024\"><path d=\"M213.333333 341.333333V230.4A102.4 102.4 0 0 1 315.733333 128h392.533334A102.4 102.4 0 0 1 810.666667 230.4V341.333333h85.333333v85.333334h-85.333333v68.266666a102.4 102.4 0 0 1-102.4 102.4H315.733333A102.4 102.4 0 0 1 213.333333 494.933333V426.666667H128V341.333333h85.333333z m102.4-128a17.066667 17.066667 0 0 0-17.066666 17.066667v264.533333c0 9.386667 7.68 17.066667 17.066666 17.066667h392.533334a17.066667 17.066667 0 0 0 17.066666-17.066667V230.4a17.066667 17.066667 0 0 0-17.066666-17.066667H315.733333z m89.6 256a64 64 0 1 1 0-128 64 64 0 0 1 0 128z m213.333334 0a64 64 0 1 1 0-128 64 64 0 0 1 0 128z m-334.464 298.666667L213.333333 830.037333V938.666667H128v-147.370667L252.16 682.666667h519.68L896 791.296V938.666667h-85.333333v-108.629334L739.797333 768H284.16z\"  ></path></symbol><symbol id=\"clipper\" viewBox=\"0 0 1024 1024\"><path d=\"M334.4 107.3L317.3 115.8c-7 3.6-14.8 9.7-20 18.7-5.4 9.1-8.1 20.3-8.1 33.4 0.1 18.9 6.3 42.6 20.8 71.6l118.4 251.6 60.4-107.5L334.4 107.3z m319.6 768l0.2 0.3c8.9 16.8 18.3 29.9 31.2 39.1 12.9 9.2 28.4 12.7 43.5 12.6 28.3 0 58.2-7.4 80.7-29.7 22.5-22.3 34.9-56.9 34.8-104.4 0.1-0.5 0.1-1.1 0.2-1.6-0.1-47.6-17.3-82.8-44.8-103.7-27.5-21.1-61.6-28.3-95.2-31.5-23.9-2.1-41.4-3.4-53.5-7.6-11.9-4.4-20.1-9.8-30.9-26.3l-27.8-49.9-47.9 85.1 109.5 217.6z m110.2-127.1c23.4 43.9 7 98.4-37 121.8l-84.9-158.6c43.8-23.6 98.4-7 121.9 36.8zM410.4 622.5c-10.6 16.3-18.8 21.9-30.7 26.2-12.1 4.2-29.7 5.4-53.6 7.5-33.5 3.3-67.8 10.4-95.3 31.4-27.8 21.1-45 56.8-44.6 105.2-0.2 47.5 12.1 82.1 34.6 104.6 22.5 22.2 52.4 29.8 80.6 29.7 15.4 0.1 30.8-3.5 43.7-12.6 12.8-9.2 22.3-22.3 31.2-38.9v-0.4L495 640.4l7-14c3.5-7.2 12.2-10 19.3-6.5 1 0.6 1.7 1.2 2.6 1.9l0.4 0.2 126-224.8 73.2-157.5c14.5-29.2 20.7-52.7 20.7-71.8 0.1-13-2.6-24.2-8-33.3-5.3-9-12.9-15.2-20.1-18.7l-17.1-8.7-246.4 440-42.2 75.3z m-19.6 87.7l-86.2 158c-43.7-23.8-59.8-78.5-35.9-122 23.9-43.6 78.6-59.7 122.1-36z\"  ></path></symbol><symbol id=\"OneNote\" viewBox=\"0 0 1084 1024\"><path d=\"M1000.75096677 598.6589525c9.54840938 60.17244563 9.66485344 121.88777437 3.28954312 182.23488656-4.54131656 32.60432437-43.43361844 30.33366656-67.974195 34.90409344-3.60976406 17.11727062-3.26043281 35.89386844-12.51773156 51.11892375-18.95126344 11.84817844-42.44384438 9.05352187-63.57843375 9.95596313-77.20238344-0.72777469-154.40476688-0.37844344-231.49070531-0.37844251-0.11644406 27.975675-0.11644406 56.03868281-0.11644406 84.01435782H562.48479739C402.31605177 931.28128719 241.71064208 904.47005188 81.33811958 876.58170969 81.22167552 631.75816437 81.22167552 387.02195187 81.22167552 142.19840562 242.64219364 114.13539781 404.12093427 86.71283187 565.39589802 58.0384925h62.96710218c0 28.00478625 0 56.03868281 0.11644407 84.04346906 98.56986 3.78443062 198.27505031-7.94730375 296.11713468 6.75375281 3.49332094 11.0912925 7.1613075 22.15347469 10.85840532 33.15743438 24.8607975 4.39576125 64.83020625 1.309995 68.73108093 34.90409344 8.00552625 61.42421906 2.41621313 123.51799125-1.60110562 185.02954312 5.70575719 65.35420406 9.60663094 131.81462719-1.83399281 196.73216719z m-530.98471781-268.63634719c-21.13458938 0.96066281-42.15273375 2.12510344-63.14176782 3.26043188-0.436665 77.66815969 0.40755375 155.42365125-0.436665 233.09181093-39.99852-75.89238844-81.80192156-150.70766906-122.99399343-225.84317062l-65.09220563 3.46420969c0.23288813 106.63360875 0.34933219 213.38366156 0 319.98815906 19.09681875 1.13532937 38.04808219 2.29976906 57.23223469 3.5515425-0.23288813-77.66815969 1.13532937-155.42365125-1.16444063-233.09181188 40.55162906 81.04503562 85.87746281 159.52830281 127.82641969 239.78734219 22.50280594 1.25177344 45.09294562 2.53265719 67.77041813 3.49332094V330.02260531zM933.65010177 782.87338719l41.36673656-2.56176844c1.48466156-54.84513187 1.54288313-109.74848625 0-164.65183969-13.85683781-0.66955312-27.68456531-1.48466156-41.36673656-2.06688187-0.11644406 56.4462375-0.11644406 112.77603 0 169.28049z m41.36673656-566.20903219a1960.10213813 1960.10213813 0 0 0-72.71928844-1.54288406c-0.11644406-13.97328187-0.11644406-27.83011969 0-41.80340157h-273.93454968v73.67995125c66.69331031 0.11644406 133.32839906 0 200.07993187 0-0.11644406 13.97328187-0.11644406 27.83011969 0 41.74518094-66.75153281 0-133.38662156-0.05822156-200.07993187 0v52.72002844c66.69331031 0.11644406 133.32839906 0 200.07993187 0-0.11644406 13.97328187-0.11644406 27.83011969 0 41.80340156-66.75153281 0-133.38662156-0.11644406-200.07993187 0v52.66180688c66.69331031 0.11644406 133.32839906 0 200.07993187 0-0.11644406 13.97328187-0.11644406 27.80100937-0.11644406 41.77429125-66.63508875 0-133.2701775-0.11644406-199.96348781 0v52.66180687c66.69331031 0.11644406 133.32839906 0 200.07993187 0-0.11644406 13.97328187-0.11644406 27.83011969 0 41.80340156-66.75153281 0-133.38662156-0.11644406-200.07993187 0v52.66180688c66.69331031 0.11644406 133.32839906 0 200.07993187 0-0.11644406 13.97328187-0.11644406 27.83011969 0 41.77429125-66.75153281 0-133.38662156-0.08733281-200.07993187 0v52.69091719c66.69331031 0.05822156 133.32839906 0 200.07993187 0-0.11644406 13.97328187-0.11644406 27.80100937 0 41.71606968-66.75153281 0-133.38662156-0.05822156-200.07993187 0v84.21813469c91.32122062 0 182.61333 0.05822156 273.93454968 0 0.05822156-153.88076813 0-307.70331469 0-461.58408281a2226.70071469 2226.70071469 0 0 0 72.31173469-1.3682175c2.35799156-55.19446406 1.77577125-110.35981688 0.40755375-165.6125025z m-0.17466562 199.43949l-41.19207094-1.83399375c-0.11644406 56.30068219-0.11644406 112.63047563 0 168.98937937 13.56572812-0.4948875 27.18967781-1.13532937 40.84273875-1.77577124 2.47443562-55.10713125 2.06688094-110.18515125 0.34933219-165.37961438z\" fill=\"#733781\" ></path></symbol><symbol id=\"youdao\" viewBox=\"0 0 1024 1024\"><path d=\"M552.96 545.28h-115.2c-23.04 0-23.04 0-23.04-23.04v-40.96H563.2v-161.28c0-12.8-7.68-12.8-15.36-12.8h-99.84c-17.92 0-23.04-2.56-23.04-23.04V243.2h140.8c-2.56-12.8-2.56-25.6-2.56-40.96h53.76c2.56 0 7.68 10.24 7.68 15.36 2.56 28.16 0 28.16 30.72 28.16H691.2c56.32 0 89.6 30.72 89.6 89.6v143.36c23.04-2.56 23.04 10.24 23.04 25.6-2.56 46.08 10.24 40.96-43.52 40.96h-89.6c7.68 25.6 15.36 46.08 23.04 66.56 17.92 43.52 51.2 71.68 97.28 87.04 10.24 2.56 12.8 7.68 12.8 17.92v46.08c-51.2-5.12-92.16-25.6-128-58.88-35.84-35.84-51.2-79.36-64-128-30.72 104.96-92.16 171.52-207.36 186.88v-58.88c2.56-2.56 0 0 17.92-7.68 61.44-20.48 99.84-64 120.32-122.88 7.68-5.12 7.68-15.36 10.24-28.16z m74.24-235.52v163.84c0 2.56 7.68 7.68 12.8 7.68h61.44c12.8 0 17.92-5.12 17.92-17.92v-117.76c0-23.04-12.8-35.84-35.84-35.84h-56.32zM291.84 204.8H353.28v181.76h53.76v64h-15.36c-38.4 2.56-38.4 2.56-38.4 40.96v256c0 23.04 0 23.04-23.04 23.04h-35.84c-2.56-192-2.56-378.88-2.56-565.76zM248.32 755.2H192V212.48h56.32v542.72z\" fill=\"#FFFFFF\" ></path><path d=\"M998.4 711.68V25.6H296.96c-102.4 7.68-202.24 89.6-250.88 212.48-10.24 28.16-17.92 58.88-20.48 87.04v371.2c0 58.88 23.04 120.32 64 176.64 20.48 25.6 43.52 48.64 69.12 66.56 35.84 25.6 76.8 40.96 115.2 48.64h478.72c69.12-10.24 140.8-51.2 192-122.88 28.16-48.64 48.64-102.4 53.76-153.6z\" fill=\"#3BA1FF\" ></path><path d=\"M637.44 250.88l53.76 53.76s-23.04 25.6-53.76 53.76L299.52 691.2l-53.76 53.76s2.56-35.84 5.12-76.8l7.68-143.36c2.56-40.96 28.16-99.84 58.88-130.56l181.76-176.64c30.72-28.16 79.36-28.16 110.08 0l28.16 33.28z\" fill=\"#FFFFFF\" ></path><path d=\"M775.68 389.12c30.72 30.72 30.72 79.36 0 107.52l-181.76 181.76c-30.72 30.72-89.6 56.32-130.56 56.32l-143.36 7.68c-40.96 2.56-76.8 2.56-76.8 2.56s25.6-25.6 56.32-53.76l337.92-332.8c30.72-30.72 53.76-53.76 56.32-53.76l53.76 53.76 28.16 30.72z\" fill=\"#EBF6FA\" ></path></symbol><symbol id=\"coda\" viewBox=\"0 0 1024 1024\"><path d=\"M181.81808602 932.04571968c0 48.59204229 30.29378395 81.52890833 72.17646611 81.52890834 37.00314539 0 60.3842542-21.14465413 64.85716095-52.45500792h-34.97000572c-2.8463958 14.02866529-14.231979 21.75459655-29.68384154 21.75459655-23.38110881 0-37.6130878-21.55128285-37.61308779-51.03181067 0-29.48052783 14.231979-51.03181068 37.61308779-51.03181067 15.24854883 0 26.83744574 7.72593127 29.68384154 21.75459655H318.85171308c-4.47290806-31.31035378-27.85401556-52.45500793-64.85716095-52.45500792-41.88268085 0.20331371-72.17646482 33.34349346-72.17646611 81.93553574zM486.38243781 932.04571968c0-47.77878617-30.09047025-81.52890833-73.59966337-81.52890833S339.18311109 884.06361851 339.18311109 932.04571968c0 47.77878617 30.09047025 81.52890833 73.59966335 81.52890834 43.71250683 0 73.59966337-33.75012217 73.59966337-81.52890834z m-34.76669201 0c0 29.48052783-13.62203659 51.43843809-38.62965764 51.4384381s-38.62965764-21.75459655-38.62965634-51.4384381c0-29.48052783 13.62203659-51.43843809 38.62965634-51.43843809s38.62965764 21.75459655 38.62965764 51.43843809zM620.56966908 1010.11829111h34.97000571V781.39005605h-34.97000571V867.18855872c-6.91267515-8.7425011-22.36453899-16.87506109-39.44291377-16.87506107-45.33901907 0-71.7698374 37.00314539-71.7698374 81.52890833 0 44.93239037 26.43081831 81.52890833 71.7698374 81.52890835 17.07837479 0 32.53023733-8.13255998 39.44291377-16.8750611v13.62203788z m0-48.5920436c-5.89610531 12.40215302-19.11151446 20.73802672-33.54680846 20.73802673-26.83744574 0-42.89925069-21.55128285-42.8992507-50.21855456 0-28.8705854 16.06180496-50.21855455 42.8992507-50.21855455 14.231979 0 27.65070187 8.53918739 33.54680846 20.73802673v58.96105565zM817.78423534 1010.11829111V907.85135605c0-35.57994814-22.9744801-57.33454469-62.01076645-57.3345447-31.71698121 0-56.11465985 20.1280843-60.79088163 46.76221632h33.54680847c3.86296563-10.97895578 12.60546674-16.87506109 26.43081832-16.87505978 19.9247706 0 29.68384154 12.40215302 29.68384153 28.05732929v12.60546803c-6.30273402-4.47290806-21.95791026-9.55575723-35.98657555-9.55575854-34.15674959 0-60.3842542 20.73802672-60.3842542 50.21855455 0 31.92029621 26.22750462 51.03181068 57.94448582 51.03181068 17.48500222 0 33.14017975-6.30273402 38.42634393-11.58889691v8.74250111l33.14017976 0.20331501z m-33.14017976-41.47605345c-3.86296563 9.75907094-17.68831722 15.85849124-31.71698249 15.85849125-15.65517755 0-32.12360992-6.70936144-32.12360862-22.56785269 0-15.45186254 16.46843237-22.16122398 32.12360862-22.16122526 14.02866529 0 27.85401556 5.89610531 31.71698249 15.85849124v13.01209546zM766.54911095-2.99530225H233.0532104c-31.92029621 0-58.14779953 26.02418961-58.14779953 57.94448582v580.25811689c0 31.92029621 26.22750462 57.94448582 58.14779953 57.9444858h533.49590055c31.92029621 0 58.14779953-26.02418961 58.14779954-57.9444858v-27.04076074c-1.01656983-34.76669201-2.03313968-107.34978423-2.03313968-140.28665029 0-18.29825834-13.62203659-33.75012217-31.10704008-33.75012216-19.31482817 0-31.92029621 11.58889691-41.67936716 21.34796913-29.07390041 26.02418961-72.78640723 30.90372638-110.60280873 24.19436366-17.48500222-3.86296563-33.95343588-8.7425011-47.57547246-17.48500222-41.67936716-24.19436494-68.9234416-69.53338272-68.9234416-117.9221126 0-48.38872859 27.24407445-92.91449154 68.9234416-117.92211131 14.63860641-8.7425011 31.10704009-13.62203659 47.57547246-17.48500351 36.79983168-6.70936144 81.52890833-2.03313968 110.60280873 24.19436494 10.57232707 9.75907094 23.17779381 21.34796914 41.67936716 21.34796785 17.48500222 0 31.10704009-15.45186254 31.10704008-33.75012087 0-31.92029621 1.01656983-105.31664457 2.03313968-140.28665029v-25.21093478c0-32.12360992-26.22750462-58.14779953-58.14779954-58.14779952z\" fill=\"#F46A54\" ></path></symbol><symbol id=\"smms\" viewBox=\"0 0 1024 1024\"><path d=\"M794.819048 136.533333h19.504762a48.761905 48.761905 0 0 1 48.761904 48.761905v604.647619a48.761905 48.761905 0 0 1-48.761904 48.761905h-19.504762a48.761905 48.761905 0 0 1-48.761905-48.761905V185.295238a48.761905 48.761905 0 0 1 48.761905-48.761905z m-195.047619 234.057143h19.504761a48.761905 48.761905 0 0 1 48.761905 48.761905v370.590476a48.761905 48.761905 0 0 1-48.761905 48.761905h-19.504761a48.761905 48.761905 0 0 1-48.761905-48.761905V419.352381a48.761905 48.761905 0 0 1 48.761905-48.761905z m-195.047619 117.028572h19.504761a48.761905 48.761905 0 0 1 48.761905 48.761904v253.561905a48.761905 48.761905 0 0 1-48.761905 48.761905h-19.504761a48.761905 48.761905 0 0 1-48.761905-48.761905V536.380952a48.761905 48.761905 0 0 1 48.761905-48.761904z m-195.04762 117.028571h19.504762a48.761905 48.761905 0 0 1 48.761905 48.761905v136.533333a48.761905 48.761905 0 0 1-48.761905 48.761905h-19.504762a48.761905 48.761905 0 0 1-48.761904-48.761905v-136.533333a48.761905 48.761905 0 0 1 48.761904-48.761905z\" fill=\"#00C074\" ></path></symbol><symbol id=\"imgur\" viewBox=\"0 0 2927 1024\"><path d=\"M102.789113 184.614534C45.176197 184.614534 0 144.046254 0 92.307267 0 41.573644 46.311885 0 102.789113 0c56.467919 0 101.653424 41.582953 101.653424 92.307267 0 51.738987-45.185506 92.307267-101.653424 92.307267m0 61.299249c59.856366 0 88.090326 37.542881 88.090326 115.654043v321.56739c0 78.111162-27.088962 116.668715-88.090326 116.668715-60.982746 0-89.226014-38.557553-89.226014-116.668715V361.558517c0-78.111162 28.243268-115.654043 89.226014-115.654043M1075.27354 799.81324c-60.973437 0-88.090326-38.557553-88.090325-116.668715V502.579371c0-81.155179-23.719132-121.714151-81.332049-121.71415-64.371193 0-89.216705 40.568281-89.216705 119.712732v182.575881c0 78.111162-28.233959 116.668715-89.226014 116.668715-61.010673 0-89.235323-38.557553-89.235323-116.668715V502.58868c0-81.155179-22.583444-121.714151-80.19636-121.71415-64.371193 0-89.235323 40.568281-89.235323 119.712732v182.57588c0 78.111162-28.243268 116.668715-89.216705 116.668716-61.001364 0-89.226014-38.557553-89.226014-116.668716V361.595753c0-78.111162 28.22465-115.654043 89.226014-115.654043 35.010854 0 63.235505 16.22545 84.692569 50.715005 37.282231-36.500282 82.467737-52.735041 142.333412-52.735041 76.798604 0 135.537899 25.366812 177.325649 75.067144 54.21516-53.768331 106.17756-75.067144 179.587716-75.067144 123.110489 0 201.035472 72.023127 201.035473 209.981346v229.260122c0 78.101853-28.22465 116.640788-89.226015 116.640789m705.942074 104.474028c-51.943783 78.111162-144.567553 119.703424-271.075798 119.703423-162.654788 0-260.929074-53.749714-260.929074-116.650097 0-40.577589 32.758095-70.003091 76.798603-70.003091 44.059126 0 97.157215 42.616243 190.898057 42.616243 86.973255 0 138.917038-47.670988 138.917038-134.914201 0-6.078726-1.12638-12.194687-1.12638-19.278777-38.380684 49.709642-91.488082 75.067144-161.509791 75.067144-140.062035 0-257.522009-115.644734-257.522008-277.936474 0-162.319667 125.372557-283.047072 276.735623-283.047071 60.973437 0 106.158943 18.264104 142.296176 56.803039 20.349303-33.474882 51.97171-50.715005 79.069981-50.715005 60.992055 0 89.226014 37.542881 89.226014 115.654043v321.56739c0.009309 96.365957-6.767586 169.394447-41.778441 221.133434m-245.085289-512.288577c-65.516191 0-116.352212 50.733623-116.352212 130.87413 0 82.160543 49.709642 133.89022 116.361521 133.89022 66.633261 0 117.441356-52.753659 117.441355-133.89022 0-80.149815-51.943783-130.87413-117.450664-130.87413m646.039163 416.946601c-172.792204 0-268.804422-81.173797-268.804422-234.333485V361.577135c0-78.111162 28.233959-115.654043 89.235323-115.654043 59.856366 0 88.090326 37.542881 88.090326 115.654043V542.151597c0 75.067144 19.222923 116.650097 91.488082 116.650098 72.293086 0 91.488082-41.582953 91.488082-116.650098V361.577135c0-78.111162 28.243268-115.654043 89.226014-115.654043 61.010673 0 88.118252 37.542881 88.118252 115.654043v213.025363c0 153.178306-96.012218 234.333485-268.841657 234.333485m594.132616-387.5211c-49.709642 21.298813-55.350848 40.577589-55.350849 95.360594v166.35043c0 78.120471-28.243268 116.678024-89.226014 116.678024-60.992055 0-89.226014-38.557553-89.226014-116.668715V361.558517c0-78.111162 28.233959-115.654043 89.226014-115.654043 35.001545 0 63.244814 16.22545 84.711187 50.715006 37.263613-35.485609 79.060672-53.759023 120.867039-53.759023 49.691024 0 90.361703 36.5189 90.361703 79.125834 0 63.915056-79.069981 68.979109-151.363066 99.419283\" fill=\"#2c2c2c\" ></path><path d=\"M102.37952 187.975055C44.999327 187.975055 0 147.565026 0 96.040145 0 45.520627 46.135016 4.095926 102.37952 4.095926c56.244505 0 101.243832 41.424701 101.243832 91.944219 0 51.524881-44.999327 91.93491-101.243832 91.93491\" fill=\"#2c2c2c\" ></path></symbol><symbol id=\"yuque\" viewBox=\"0 0 1024 1024\"><path d=\"M877.408 357.312c-10.592-43.104 10.592-111.712 78.688-136.096l-72.8-4s-27.488-98.4-153.888-107.2c-126.4-8.8-209.088-3.296-209.088-3.296s93.792 60.8 56.192 169.184c-27.392 57.504-70.496 104.608-116.704 158.304-1.408 1.504-2.688 2.816-3.808 4.096C303.488 613.6 64 892.416 64 892.416c263.712 70.496 440.384-6.784 544.896-99.616 22.016-0.192 38.496-0.288 49.6-0.288 145.6 0 268.704-128.608 263.712-271.616-3.488-98.4-34.208-120.608-44.8-163.616z\"  ></path></symbol><symbol id=\"privacy\" viewBox=\"0 0 1024 1024\"><path d=\"M322.285714 877.714286H121.325714c-23.277714 0-42.697143-15.177143-47.195428-35.364572A40.886857 40.886857 0 0 1 73.142857 833.444571V117.412571C73.142857 92.964571 94.701714 73.142857 121.307429 73.142857h561.938285c19.968 0 37.065143 11.154286 44.397715 27.044572 2.432 5.284571 3.785143 11.117714 3.785142 17.225142V365.714286h73.142858V86.619429C804.571429 38.765714 760.649143 0 706.450286 0H98.121143C43.922286 0 0 38.784 0 86.619429v777.636571C0 912.091429 43.922286 950.857143 98.121143 950.857143h224.164571v-73.142857z\"  ></path><path d=\"M274.285714 219.428571h365.714286v73.142858H274.285714zM274.285714 402.285714h219.428572v73.142857H274.285714zM219.428571 256a36.571429 36.571429 0 0 1-73.142857 0 36.571429 36.571429 0 0 1 73.142857 0zM214.857143 438.857143a36.571429 36.571429 0 0 1-73.142857 0 36.571429 36.571429 0 0 1 73.142857 0z\"  ></path><path d=\"M662.857143 843.428571m-54.857143 0a54.857143 54.857143 0 1 0 109.714286 0 54.857143 54.857143 0 1 0-109.714286 0Z\"  ></path><path d=\"M841.142857 658.285714h-18.395428c0.054857-0.731429 0.109714-1.426286 0.109714-2.176a164.571429 164.571429 0 1 0-329.142857 0c0 0.749714 0.073143 1.444571 0.109714 2.176H475.428571a54.857143 54.857143 0 0 0-54.857142 54.857143v256a54.857143 54.857143 0 0 0 54.857142 54.857143h365.714286a54.857143 54.857143 0 0 0 54.857143-54.857143V713.142857a54.857143 54.857143 0 0 0-54.857143-54.857143z m-274.285714-2.176c0-50.505143 40.923429-91.428571 91.428571-91.428571s91.428571 40.923429 91.428572 91.428571c0 0.749714-0.128 1.462857-0.219429 2.176h-182.436571c-0.073143-0.713143-0.201143-1.426286-0.201143-2.176zM822.857143 932.571429a18.285714 18.285714 0 0 1-18.285714 18.285714H512a18.285714 18.285714 0 0 1-18.285714-18.285714V749.714286a18.285714 18.285714 0 0 1 18.285714-18.285715h292.571429a18.285714 18.285714 0 0 1 18.285714 18.285715v182.857143z\"  ></path></symbol><symbol id=\"mail\" viewBox=\"0 0 1024 1024\"><path d=\"M948.90666667 147.91111111L75.09333333 147.91111111C35.04355555 147.91111111 2.27555555 180.67911111 2.27555555 220.72888889l0 582.54222222c0 40.04977778 32.768 72.81777778 72.81777778 72.81777778l873.81333334 0c40.04977778 0 72.81777778-32.768 72.81777778-72.81777778L1021.72444445 220.72888889C1021.72444445 180.67911111 988.95644445 147.91111111 948.90666667 147.91111111zM887.01155555 220.72888889L512 537.48622222 136.98844445 220.72888889 887.01155555 220.72888889zM75.09333333 803.27111111L75.09333333 264.41955555l436.90666667 371.37066667L948.90666667 264.41955555 948.90666667 803.27111111 75.09333333 803.27111111z\"  ></path></symbol><symbol id=\"115pan\" viewBox=\"0 0 1024 1024\"><path d=\"M701.004655 35.208896c29.184693-0.150426 57.568137-7.509021 84.900645-17.220197 20.924565 9.561773-3.704368 30.886451-7.559163 44.903699-9.81146 18.4717-18.371416 37.594223-29.084409 55.516408-10.562567 15.618723-29.785374 24.128537-48.4075 23.228027-75.139328-0.100284-150.228513 0-225.367841-0.050142-21.425985-0.851391-42.250266 12.565177-50.960649 32.03767-11.763928 24.329105-25.080212 47.957245-36.193317 72.586179 67.680449 13.216 137.012514 25.430182 198.886723 57.31845 58.670238 29.9358 109.880573 76.740802 139.615804 136.01172 37.294394 72.836889 39.797401 162.54298 5.106298 236.882082C696.149067 753.612827 625.013937 810.830993 545.56956 838.764182c-95.764064 32.989345-205.795063 33.939997-297.453622-12.214183-0.550539-0.751107-1.701758-2.252297-2.302439-3.053546 89.155553 11.914354 183.668113-3.604084 260.560364-51.611471 49.659003-30.836309 89.305979-80.195483 101.069907-138.214898 11.062963-44.803415 2.753717-93.411483-22.577205-131.957381-38.845727-60.021002-102.421694-100.519368-169.70203-121.895211-63.825654-21.926382-131.005706-30.036084-197.685361-37.594223 47.306422-93.110631 93.66117-186.721659 140.567479-280.032858 8.760524-17.020652 28.234041-27.432793 47.206138-26.78197C503.81969 35.208896 602.437755 35.609009 701.004655 35.208896z\"  ></path></symbol><symbol id=\"github\" viewBox=\"0 0 1024 1024\"><path d=\"M512 73.142857q119.428571 0 220.285714 58.857143T892 291.714286 950.857143 512q0 143.428571-83.714286 258T650.857143 928.571429q-15.428571 2.857143-22.857143-4t-7.428571-17.142858q0-1.714286 0.285714-43.714285t0.285714-76.857143q0-55.428571-29.714286-81.142857 32.571429-3.428571 58.571429-10.285715t53.714286-22.285714 46.285714-38 30.285714-60T792 489.142857q0-68-45.142857-117.714286 21.142857-52-4.571429-116.571428-16-5.142857-46.285714 6.285714t-52.571429 25.142857l-21.714285 13.714286q-53.142857-14.857143-109.714286-14.857143t-109.714286 14.857143q-9.142857-6.285714-24.285714-15.428571T330.285714 262.571429 281.714286 254.857143q-25.714286 64.571429-4.571429 116.571428-45.142857 49.714286-45.142857 117.714286 0 48.571429 11.714286 85.714286t30 60 46 38.285714 53.714285 22.285714 58.571429 10.285715q-22.285714 20.571429-28 58.857143-12 5.714286-25.714286 8.571428t-32.571428 2.857143-37.428572-12.285714T276.571429 728q-10.857143-18.285714-27.714286-29.714286t-28.285714-13.714285l-11.428572-1.714286q-12 0-16.571428 2.571428t-2.857143 6.571429 5.142857 8 7.428571 6.857143l4 2.857143q12.571429 5.714286 24.857143 21.714285t18 29.142858l5.714286 13.142857q7.428571 21.714286 25.142857 35.142857t38.285714 17.142857 39.714286 4 31.714286-2l13.142857-2.285714q0 21.714286 0.285714 50.571428t0.285714 31.142857q0 10.285714-7.428571 17.142858t-22.857143 4q-132.571429-44-216.285714-158.571429T73.142857 512q0-119.428571 58.857143-220.285714T291.714286 132 512 73.142857zM239.428571 703.428571q1.714286-4-4-6.857142-5.714286-1.714286-7.428571 1.142857-1.714286 4 4 6.857143 5.142857 3.428571 7.428571-1.142858z m17.714286 19.428572q4-2.857143-1.142857-9.142857-5.714286-5.142857-9.142857-1.714286-4 2.857143 1.142857 9.142857 5.714286 5.714286 9.142857 1.714286z m17.142857 25.714286q5.142857-4 0-10.857143-4.571429-7.428571-9.714285-3.428572-5.142857 2.857143 0 10.285715t9.714285 4z m24 24q4.571429-4.571429-2.285714-10.857143-6.857143-6.857143-11.428571-1.714286-5.142857 4.571429 2.285714 10.857143 6.857143 6.857143 11.428571 1.714286z m32.571429 14.285714q1.714286-6.285714-7.428572-9.142857-8.571429-2.285714-10.857142 4t7.428571 8.571428q8.571429 3.428571 10.857143-3.428571z m36 2.857143q0-7.428571-9.714286-6.285715-9.142857 0-9.142857 6.285715 0 7.428571 9.714286 6.285714 9.142857 0 9.142857-6.285714z m33.142857-5.714286q-1.142857-6.285714-10.285714-5.142857-9.142857 1.714286-8 8.571428t10.285714 4.571429 8-8z\"  ></path></symbol><symbol id=\"iconfont\" viewBox=\"0 0 1025 1024\"><path d=\"M1.703827 0h1022.296173v1022.296173H1.703827z\" fill=\"#E94618\" ></path><path d=\"M501.799188 865.690622c-38.777398-23.478735-30.512133-64.416586-55.495348-90.18356-45.955621-47.388539-126.495521-49.818196-173.427434-104.054416-157.356938-181.844339-6.25986-521.808932 291.356113-471.721531 173.442769 29.18826 305.577957 272.184652 187.299993 443.973005-50.872865 73.896679-148.708313 79.747621-208.112239 159.553171-17.406296 17.273398-8.592399 60.780619-41.621085 62.433331zM252.06416 470.276686c-6.982283 131.959694 180.363714 125.277285 180.363714 6.937983 0-60.245617-54.101617-108.603634-124.866663-83.245577-49.779008 17.835661-53.437125 37.388779-55.497051 76.307594z m381.53797 97.11984c158.950017 44.573817 162.964233-230.536306 0-173.427434-77.096466 27.017584-68.110483 154.329238 0 173.427434z m-159.551468 83.247281c25.407468 5.697597 32.996313-68.502363 6.93628-69.374722-14.470602 14.327481-48.605072 59.225025-6.93628 69.374722z m62.429924 0h13.879374c17.914037-28.413018-6.004286-65.42014-34.686509-69.374722 2.320612 27.740007-8.91783 69.037364 20.807135 69.374722z\" fill=\"#FFFFFF\" ></path></symbol><symbol id=\"powerpack\" viewBox=\"0 0 1024 1024\"><path d=\"M384.156483 720.268521l275.365669-243.511323h-124.486442l104.827678-173.025719-275.394264 243.525621h124.50074l-104.813381 173.011421z\"  ></path><path d=\"M355.790673 52.070705v52.056407H251.663561A52.070705 52.070705 0 0 0 199.592857 156.197816v815.717182a52.056407 52.056407 0 0 0 52.070704 52.070705h520.664154a52.056407 52.056407 0 0 0 52.070704-52.070705V156.212114a52.070705 52.070705 0 0 0-52.070704-52.070705h-104.127112V52.070705A52.070705 52.070705 0 0 0 616.129899 0H407.832783a52.070705 52.070705 0 0 0-52.04211 52.070705z m373.159085 121.526905a26.04965 26.04965 0 0 1 26.021055 26.021055v728.918377a26.035352 26.035352 0 0 1-26.035352 25.978163H295.055815a26.035352 26.035352 0 0 1-26.035352-26.035352V199.618665a26.035352 26.035352 0 0 1 26.035352-26.035353z\"  ></path></symbol><symbol id=\"store\" viewBox=\"0 0 1024 1024\"><path d=\"M925.355 188.459A132.01 132.01 0 0 0 794.709 77.78H229.291A132.01 132.01 0 0 0 98.645 188.416l-38.912 234.71A131.84 131.84 0 0 0 112.3 528v285.696a132.608 132.608 0 0 0 132.437 132.48h534.4a132.608 132.608 0 0 0 132.437-132.48V527.659A135.68 135.68 0 0 0 963.84 418.9L925.355 188.46zM593.28 895.019H430.677v-162.56H593.28v162.56z m267.221-81.28c0 44.8-36.437 81.28-81.28 81.28H644.48v-188.16a25.6 25.6 0 0 0-25.6-25.6H405.12a25.6 25.6 0 0 0-25.6 25.6v188.16H244.779a81.323 81.323 0 0 1-81.28-81.28v-261.59c9.216 2.048 18.688 3.286 28.501 3.286 43.733 0 82.603-21.334 106.667-54.144a132.096 132.096 0 0 0 213.333 0 132.096 132.096 0 0 0 213.333 0A132.096 132.096 0 0 0 832 555.435c9.813 0 19.285-1.238 28.501-3.286v261.59z m16-322.944c-0.469 0.17-0.853 0.469-1.28 0.682-12.544 7.936-27.306 12.715-43.178 12.715-44.715 0-81.067-36.352-81.067-81.067a25.6 25.6 0 1 0-51.2 0c0 44.715-36.395 81.067-81.067 81.067s-81.066-36.352-81.066-81.067a25.6 25.6 0 1 0-51.2 0c0 44.715-36.395 81.067-81.067 81.067s-81.067-36.352-81.067-81.067a25.6 25.6 0 1 0-51.2 0c0 44.715-36.394 81.067-81.066 81.067-15.318 0-29.526-4.523-41.771-11.733-1.152-0.64-2.176-1.323-3.413-1.792-21.675-14.208-36.054-37.931-36.267-63.275l38.528-230.485a80.939 80.939 0 0 1 80.17-67.883h565.42c39.935 0 73.642 28.544 80.17 67.883l38.187 226.218c0 28.288-14.592 53.163-36.566 67.67z\"  ></path></symbol><symbol id=\"douban\" viewBox=\"0 0 1024 1024\"><path d=\"M380.47491161 431.35026569h265.5097995v92.17112492H380.47491161zM404.68277728 570.90149136c23.04278124 38.83614815 44.79102419 81.16754963 63.04401383 124.79348939h88.54641778c26.66748839-41.16631703 48.41573136-82.4620879 64.20909828-124.79348939H404.68277728z\" fill=\"#228A31\" ></path><path d=\"M512 14.8973037c-274.57156741 0-497.1026963 222.53112889-497.1026963 497.1026963s222.53112889 497.1026963 497.1026963 497.1026963 497.1026963-222.53112889 497.1026963-497.1026963-222.53112889-497.1026963-497.1026963-497.1026963zM283.51399506 279.8892879h459.43163259v47.25064692H283.51399506v-47.25064692z m471.60029235 464.2214242h-1.29453827v0.12945382H269.01516642v-47.2506469h145.50610173c-18.12353581-38.83614815-36.37652543-72.75305086-55.79459951-100.58562371l37.54160988-24.20786568H330.76464197V382.93453431h368.55504593v189.1320415h-69.12834371l37.54160989 24.20786567c-18.12353581 38.83614815-36.37652543 72.75305086-54.50006125 100.5856237h141.88139458v47.25064692z\" fill=\"#228A31\" ></path></symbol><symbol id=\"weibo\" viewBox=\"0 0 1024 1024\"><path d=\"M757.76 507.44888853c-18.2044448 0-31.85777813-9.10222187-18.2044448-27.30666666 9.10222187-18.2044448 31.85777813-72.81777813-9.10222187-109.22666667-36.40888853-36.40888853-122.88-22.7555552-182.0444448 0-59.1644448 22.7555552-54.61333333 9.10222187-54.61333333-9.10222187s31.85777813-118.32888853-63.7155552-127.43111146C302.64888853 220.72888853 138.80888853 375.46666667 84.1955552 466.48888853-2.2755552 603.02222187 11.37777813 694.0444448 11.37777813 694.0444448c9.10222187 127.43111147 200.24888853 227.5555552 427.80444374 227.5555552 200.24888853 0 364.08888853-77.36888853 414.15111146-177.49333333 4.55111147-9.10222187 9.10222187-22.7555552 13.65333334-31.85777814 9.10222187-27.30666667 13.65333333-68.26666667 0-104.6755552-22.7555552-77.36888853-95.57333333-100.1244448-109.22666667-100.1244448z m-345.8844448 359.53777814c-163.84 0-295.82222187-86.47111147-295.82222187-195.69777814s131.98222187-195.69777813 295.82222187-195.69777706 295.82222187 86.47111147 295.82222293 195.69777706c0 104.6755552-131.98222187 195.69777813-295.82222293 195.69777814zM816.9244448 430.08h9.10222187c9.10222187 0 18.2044448-4.55111147 22.7555552-13.65333333 4.55111147-13.65333333 9.10222187-31.85777813 9.10222293-45.51111147 0-63.7155552-54.61333333-118.32888853-118.3288896-118.32888853-18.2044448 0-31.85777813 4.55111147-45.5111104 9.10222186-13.65333333 4.55111147-18.2044448 18.2044448-13.65333333 31.85777814 4.55111147 13.65333333 18.2044448 18.2044448 31.85777706 13.65333333 9.10222187-4.55111147 18.2044448-4.55111147 27.30666667-4.55111147 40.96 0 72.81777813 31.85777813 72.81777813 72.81777814 0 9.10222187 0 18.2044448-4.55111146 27.30666666-9.10222187 9.10222187-4.55111147 22.7555552 9.10222293 27.30666667z\" fill=\"#FF5E6B\" ></path><path d=\"M384.56888853 543.85777813c-86.47111147 0-159.28888853 63.7155552-159.28888853 141.08444374 0 77.36888853 72.81777813 141.0844448 159.28888853 141.0844448s159.28888853-63.7155552 159.2888896-141.0844448c0-77.36888853-72.81777813-141.0844448-159.2888896-141.08444374z m-54.61333333 227.5555552c-31.85777813 0-54.61333333-22.7555552-54.61333333-54.61333333s22.7555552-54.61333333 54.61333333-54.61333333 54.61333333 22.7555552 54.61333333 54.61333333c4.55111147 31.85777813-22.7555552 54.61333333-54.61333333 54.61333333z m104.67555627-100.1244448c-4.55111147 13.65333333-18.2044448 18.2044448-31.85777814 9.10222294-9.10222187-4.55111147-13.65333333-18.2044448-4.55111146-31.85777814 4.55111147-13.65333333 18.2044448-18.2044448 31.85777813-9.10222186 9.10222187 9.10222187 13.65333333 22.7555552 4.55111147 31.85777706zM735.0044448 102.4c-27.30666667 0-54.61333333 4.55111147-77.3688896 13.65333333-18.2044448 4.55111147-31.85777813 27.30666667-22.7555552 45.51111147 4.55111147 18.2044448 27.30666667 31.85777813 45.51111147 22.7555552 18.2044448-4.55111147 40.96-9.10222187 59.16444373-9.10222187 113.77777813 0 204.8 91.02222187 204.8 204.8 0 18.2044448-4.55111147 40.96-9.10222187 59.16444374s4.55111147 40.96 22.7555552 45.51111146h9.10222294c13.65333333 0 31.85777813-9.10222187 36.40888853-27.30666666 9.10222187-27.30666667 13.65333333-54.61333333 13.65333333-81.92 0-150.18666667-127.43111147-273.06666667-282.16888853-273.06666667z\" fill=\"#FF5E6B\" ></path></symbol><symbol id=\"twitter\" viewBox=\"0 0 1024 1024\"><path d=\"M875.99179852 247.4002609c38.62190206-4.2046603 75.40814885-14.52083579 109.37294948-29.95949921A361.84545406 361.84545406 0 0 1 890.19935605 317.97848747a558.43338871 558.43338871 0 0 1 0.77737023 29.29151747c0 303.85467038-246.32409505 550.17876543-550.17876543 550.17876543-111.32576048 0-214.92701108-33.0676856-301.52255969-89.91215565a362.52055577 362.52055577 0 0 0 56.85482636 4.47262973c87.3839224 0 167.49059792-31.14853262 229.83167746-82.94300888-83.94821783-2.45250275-154.52126625-58.18690623-179.09613351-134.55754428a215.16585339 215.16585339 0 0 0 33.4094437 2.6065528c18.1824373 0 35.83152482-2.27903463 52.68447132-6.55424727C144.52071538 572.41869022 78.00604445 494.15025904 78.00604445 400.34607408c0-0.90682405 0.02200715-1.80911724 0.03430526-2.71335221 26.06358629 14.23474284 55.41983067 23.19294768 86.63373749 25.44738606C113.88870352 388.05572773 80.59512098 329.48304909 80.59512098 263.12501728c0-36.40241619 10.02619891-70.45848178 27.45392041-99.57588385 95.31167478 117.11040475 237.8422803 194.24611493 398.57797751 202.40623692a194.6577781 194.6577781 0 0 1-5.30695965-45.22404725c0-107.24343404 86.9373067-194.18074075 194.18074075-194.18074073 55.80042493 0 106.09453132 23.54441482 141.51439297 61.23166024 44.41625537-7.78211682 86.03307173-23.69717033 123.24716341-46.17423834-15.98301677 43.72950282-45.85384012 80.77142092-84.27055786 105.79225663z\" fill=\"#00A0E9\" ></path></symbol><symbol id=\"changelog\" viewBox=\"0 0 1024 1024\"><path d=\"M846.1312 981.7088H183.0912a102.4 102.4 0 0 1-102.4-102.4V236.8512a102.4 102.4 0 0 1 102.4-102.4h663.04a102.4 102.4 0 0 1 102.4 102.4v642.56a102.4 102.4 0 0 1-102.4 102.2976z m-663.04-788.48a43.6224 43.6224 0 0 0-43.6224 43.6224v642.56a43.6224 43.6224 0 0 0 43.6224 43.52h663.04a43.6224 43.6224 0 0 0 43.52-43.52V236.8512a43.6224 43.6224 0 0 0-43.52-43.6224z\" fill=\"#333333\" ></path><path d=\"M514.56 265.6256A30.0032 30.0032 0 0 1 484.4544 235.52V61.44a30.1056 30.1056 0 1 1 60.2112 0v174.08a30.0032 30.0032 0 0 1-30.1056 30.1056zM307.2 265.6256A30.0032 30.0032 0 0 1 277.0944 235.52V61.44a30.1056 30.1056 0 1 1 60.2112 0v174.08A30.0032 30.0032 0 0 1 307.2 265.6256zM727.04 265.6256A30.0032 30.0032 0 0 1 696.9344 235.52V61.44a30.1056 30.1056 0 0 1 60.2112 0v174.08A30.0032 30.0032 0 0 1 727.04 265.6256zM778.24 428.7488H256a29.3888 29.3888 0 0 1 0-58.7776h522.24a29.3888 29.3888 0 1 1 0 58.7776zM634.88 592.5888H256a29.3888 29.3888 0 0 1 0-58.7776h378.88a29.3888 29.3888 0 1 1 0 58.7776zM491.52 766.6688H256a29.3888 29.3888 0 1 1 0-58.7776h235.52a29.3888 29.3888 0 0 1 0 58.7776z\" fill=\"#333333\" ></path></symbol><symbol id=\"bear\" viewBox=\"0 0 1034 1024\"><path d=\"M275.772462 298.225849l-0.275296 27.560523-10.759719 15.663598c-104.399116 151.997106-129.307015 208.605266-175.775517 399.473206l-6.499056 26.69604 2.8919 5.207477c72.143116 129.924503 228.722492 229.823678 381.362814 243.308061 5.621709 0.496563 12.787136 1.13206 14.464643 0 2.222955-1.502553-1.674935-11.19196 0-19.638674 14.094151-71.062513 23.196945-119.802854 16.129287-161.635055v-8.130251l46.198351-10.353206 59.875699-0.265005-4.106292-9.776885c-30.578492-72.801769-33.781709-123.361126-9.349789-147.597507 12.522131-12.424362 22.939658-15.473206 58.036101-16.991196 110.381025-4.775236 184.21194-50.81407 212.196985-132.319839 17.636985-51.364663 11.112201-79.002372-18.730452-79.336845-11.997266-0.136362-18.697005-2.081447-52.15196-15.146452-69.549668-27.164302-115.179417-55.275417-152.753528-94.110231-17.572663-18.164422-28.867538-25.785246-49.885266-33.66593-37.098131-13.906332-99.904322-18.915698-162.010694-12.923497l-10.695397 1.031718-7.314653-6.941587c-33.90006-32.178814-81.415719-53.248-104.072362-46.146895-18.46802 5.786372-26.297246 28.021065-26.775799 76.038432z\" fill=\"#F4F3F3\" ></path><path d=\"M477.009045 3.71007C172.989106 30.231156-40.049206 303.047397 9.370372 602.564824c9.730573 58.985487 30.964422 118.526714 60.832804 170.58605 6.133709 10.687678 8.518754 13.116462 8.531618 8.680845 0.002573-0.957106 5.063397-22.116342 11.243417-47.021669 27.825528-112.133146 44.628905-166.80394 64.375638-209.431156 21.326472-46.038834 62.81391-115.722291 106.923096-179.585929l13.504965-19.553769 0.084904-24.699498c0.244422-69.832683 16.255357-90.22006 60.634694-77.211658 22.404503 6.56595 46.123739 20.93797 70.496482 42.714694l8.747739 7.816361 5.660301-0.730693c26.986774-3.47594 79.39602-3.650894 107.545729-0.362774 49.607397 5.799236 81.824804 18.650693 103.429146 41.261025 25.965347 27.17202 42.791879 40.661548 73.761447 59.124422 43.867337 26.155739 117.963256 56.296844 136.809487 55.653629 28.229467-0.964824 35.711357 26.559678 20.397669 75.035015-27.040804 85.596623-110.635739 137.465568-222.220865 137.887517-84.665246 0.319035-88.238955 84.248442-9.506733 223.298895 19.270754 34.036422 74.759719 125.982874 77.733949 128.807879 1.425367 1.353327 42.526874-18.043497 63.444262-29.940422 190.739296-108.477106 291.883739-326.697166 251.577246-542.776603C975.367719 164.725065 735.643658-18.85395 477.009045 3.71007\" fill=\"#DC4C4C\" ></path><path d=\"M497.308945 826.730774c-9.499015-91.344402-54.804583-137.748583-74.170533-167.526915C375.638191 587.405508 375.638191 535.155779 375.638191 535.155779s39.462593 65.952804 78.73994 89.520241c39.27992 23.567437 158.758593 25.208925 158.758593 25.208925-24.094874 2.251256-19.919116 4.795819-20.094071 28.996181 0.728121 32.564744-14.199638 82.179859 0 111.495075 23.930211 54.807156 113.404141 204.524704 113.404141 204.524704s-37.898291 13.96808-66.971658 21.964542c-10.80603 2.971658-20.351357 6.830955-26.338412 6.375558-80.82396-6.12599-113.862111-5.222915-126.379096-6.375558-7.319799-0.67409-6.864402 5.127719-6.864402-5.099417 0.838754-4.422754 1.574593-8.999879 2.225528-13.708221 7.219457-52.267739 3.962211-120.662191 15.192764-171.327035z\" fill=\"#D1D1D1\" ></path></symbol><symbol id=\"notion\" viewBox=\"0 0 1024 1024\"><path d=\"M647.165837 2.332635L78.85493 44.179432C33.04873 48.138835 17.074585 78.005024 17.074585 113.810318v621.148461c0 27.886487 9.966774 51.745305 33.893858 83.625329l133.595729 173.189762c21.947382 27.886487 41.88093 33.859725 83.795993 31.880024l659.957441-39.832963c55.807107-3.959403 71.781251-29.866189 71.781251-73.658555V211.361824c0-22.630038-8.976923-29.1494-35.3957-48.468558a1594.683818 1594.683818 0 0 1-4.505528-3.276748L778.781865 32.198823C734.8871 0.387066 716.967387-3.640603 647.165837 2.332635zM283.310326 199.92734c-53.895671 3.618075-66.115209 4.437262-96.732319-20.377274l-77.822755-61.712079c-7.918807-7.987072-3.925271-17.953846 15.974144-19.933548l546.363525-39.79883c45.840333-3.993536 69.767417 11.946476 87.721263 25.872653l93.728634 67.71945c3.993536 1.979702 13.926177 13.892044 1.979702 13.892044l-564.249106 33.859725-6.963088 0.477859zM220.471864 904.189138V310.961297c0-25.906785 7.952939-37.853261 31.880024-39.867096l648.010965-37.819128c21.981515-1.979702 31.948289 11.946476 31.948289 37.819128v589.268439c0 25.906785-3.993536 47.820035-39.935361 49.799736l-620.090346 35.839427c-35.873559 1.979702-51.813571-9.932641-51.813571-41.812665z m612.171539-561.416084c3.959403 17.919713 0 35.839427-17.987979 37.887394l-29.900321 5.904972v437.991926c-25.940918 13.926177-49.833869 21.879117-69.767417 21.879116-31.914156 0-39.935361-9.966774-63.828313-39.79883l-195.444339-306.580694v296.613921l61.84861 13.96031s0 35.839427-49.902135 35.839426l-137.555132 7.95294c-3.959403-7.987072 0-27.886487 13.994443-31.845891l35.839426-9.932641v-392.185725l-49.833869-4.027669c-3.959403-17.919713 5.973238-43.792366 33.92799-45.772068l147.55604-9.966773 203.397279 310.608363v-274.768937l-51.881837-5.939105c-3.959403-21.947382 11.946476-37.853261 31.914156-39.832962l137.623398-7.987073z\" fill=\"#000000\" ></path></symbol><symbol id=\"premium\" viewBox=\"0 0 1024 1024\"><path d=\"M876.8 409.6l-136.96 42.24c-23.04 7.68-48.64-1.28-62.72-21.76L558.08 256c-21.76-32-70.4-32-92.16 0l-119.04 172.8c-14.08 20.48-39.68 29.44-62.72 21.76L147.2 409.6c-39.68-12.8-79.36 23.04-71.68 64l79.36 418.56c7.68 39.68 42.24 67.84 81.92 67.84h547.84c40.96 0 75.52-28.16 81.92-67.84L947.2 473.6c7.68-40.96-30.72-76.8-70.4-64zM638.72 608l-103.68 204.8c-3.84 8.96-12.8 14.08-23.04 14.08s-17.92-5.12-23.04-14.08l-103.68-204.8c-6.4-12.8-1.28-28.16 11.52-34.56s28.16-1.28 34.56 11.52L512 744.96l80.64-160c6.4-12.8 21.76-17.92 34.56-11.52 12.8 6.4 17.92 21.76 11.52 34.56z\" fill=\"#FF5511\" ></path><path d=\"M512 120.32m-56.32 0a56.32 56.32 0 1 0 112.64 0 56.32 56.32 0 1 0-112.64 0Z\" fill=\"#FF5511\" ></path><path d=\"M75.52 307.2m-56.32 0a56.32 56.32 0 1 0 112.64 0 56.32 56.32 0 1 0-112.64 0Z\" fill=\"#FF5511\" ></path><path d=\"M948.48 307.2m-56.32 0a56.32 56.32 0 1 0 112.64 0 56.32 56.32 0 1 0-112.64 0Z\" fill=\"#FF5511\" ></path></symbol><symbol id=\"google\" viewBox=\"0 0 1024 1024\"><path d=\"M509.8 231.8c86.2 0 144 36.8 176.9 68.5l120.5-116.1C729.9 111.7 629.7 68 509.9 68c-172.3 0-321.4 98-395 241.2L249.4 422c37-110.5 139.7-190.2 260.4-190.2z\" fill=\"#EA4335\" ></path><path d=\"M234.6 512.1c0-31.6 5.4-61.8 14.9-90.1L114.8 309.2c-31.2 60.8-49 129.7-49 202.8 0 67.4 15.1 131.2 41.9 188.4l137.1-113.6c-6.5-23.8-10.2-48.8-10.2-74.7z\" fill=\"#FBBC05\" ></path><path d=\"M509.8 792.3c-126.2 0-232.7-87.1-265-205.5L107.7 700.4C178.6 851.5 331.8 956 509.9 956c120.6 0 221.7-40.3 295.4-109L667.1 746.6c-38.2 26.8-89.5 45.7-157.3 45.7z\" fill=\"#34A853\" ></path><path d=\"M936.1 522.2c0-29.1-3.1-51.3-6.9-73.5l-419.4-0.2-0.1 0.1v152.3h251.8C755.2 641 727.8 704 667 746.7L805.3 847c82.8-77.3 130.8-190.6 130.8-324.8z\" fill=\"#4285F4\" ></path></symbol></svg>',function(a){var c=(c=document.getElementsByTagName(\"script\"))[c.length-1],l=c.getAttribute(\"data-injectcss\"),c=c.getAttribute(\"data-disable-injectsvg\");if(!c){var h,t,i,o,v,s=function(c,l){l.parentNode.insertBefore(c,l)};if(l&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write(\"<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>\")}catch(c){console&&console.log(c)}}h=function(){var c,l=document.createElement(\"div\");l.innerHTML=a._iconfont_svg_string_1402208,(l=l.getElementsByTagName(\"svg\")[0])&&(l.setAttribute(\"aria-hidden\",\"true\"),l.style.position=\"absolute\",l.style.width=0,l.style.height=0,l.style.overflow=\"hidden\",l=l,(c=document.body).firstChild?s(l,c.firstChild):c.appendChild(l))},document.addEventListener?~[\"complete\",\"loaded\",\"interactive\"].indexOf(document.readyState)?setTimeout(h,0):(t=function(){document.removeEventListener(\"DOMContentLoaded\",t,!1),h()},document.addEventListener(\"DOMContentLoaded\",t,!1)):document.attachEvent&&(i=h,o=a.document,v=!1,z(),o.onreadystatechange=function(){\"complete\"==o.readyState&&(o.onreadystatechange=null,p())})}function p(){v||(v=!0,i())}function z(){try{o.documentElement.doScroll(\"left\")}catch(c){return void setTimeout(z,50)}p()}}(window);"
  },
  {
    "path": "config.json",
    "content": "{\n  \"iconfont\": \"https://at.alicdn.com/t/font_1402208_ghcp6tuu13c.js\",\n  \"chromeWebStoreVersion\": \"1.28.4\",\n  \"edgeWebStoreVersion\": \"1.28.4\",\n  \"firefoxWebStoreVersion\": \"1.28.4\",\n  \"privacyLocale\": [\"en-US\", \"zh-CN\"],\n  \"changelogLocale\": [\"en-US\", \"zh-CN\"]\n}\n"
  },
  {
    "path": "global.d.ts",
    "content": "declare module '*.md' {\n  const src: string;\n  export default src;\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"web-clipper\",\n  \"version\": \"1.42.0\",\n  \"description\": \"Universal open source web clipper.\",\n  \"bin\": {\n    \"web-clipper\": \"bin/index.js\"\n  },\n  \"scripts\": {\n    \"test\": \"vitest\",\n    \"cov\": \"vitest --coverage\",\n    \"dev\": \"webpack --config webpack/webpack.dev.js --watch\",\n    \"release\": \"ts-node script/release.ts\",\n    \"format\": \"web-clipper format\"\n  },\n  \"author\": \"DiamondYuan\",\n  \"license\": \"GPL-2.0-or-later\",\n  \"dependencies\": {\n    \"@ant-design/compatible\": \"^1.0.8\",\n    \"@ant-design/icons\": \"^4.2.2\",\n    \"@formily/antd\": \"^2.0.0-beta.47\",\n    \"@formily/core\": \"^2.0.0-beta.47\",\n    \"@formily/react\": \"^2.0.0-beta.47\",\n    \"@shihengtech/hooks\": \"^0.0.16\",\n    \"@web-clipper/area-selector\": \"^0.1.3\",\n    \"@web-clipper/chrome-promise\": \"^0.1.2\",\n    \"@web-clipper/highlight\": \"^0.1.3\",\n    \"@web-clipper/readability\": \"^0.3.0\",\n    \"@web-clipper/remark-pangu\": \"^1.0.2\",\n    \"@web-clipper/shared\": \"^0.0.20\",\n    \"@web-clipper/turndown\": \"^0.4.8\",\n    \"antd\": \"4.16.3\",\n    \"classnames\": \"^2.2.6\",\n    \"codemirror\": \"^5.47.0\",\n    \"copy-to-clipboard\": \"^3.2.0\",\n    \"cos-js-sdk-v5\": \"^1.8.1\",\n    \"dayjs\": \"^1.10.4\",\n    \"dva\": \"^2.6.0-beta.19\",\n    \"dva-loading\": \"^3.0.19\",\n    \"dva-model-creator\": \"^0.4.3\",\n    \"filenamify\": \"^4.1.0\",\n    \"form-data\": \"^2.3.3\",\n    \"history\": \"4.10.1\",\n    \"hypermd\": \"^0.3.11\",\n    \"immutability-helper\": \"^3.0.1\",\n    \"jquery\": \"^3.4.0\",\n    \"lodash\": \"^4.17.20\",\n    \"mobx\": \"^5.15.1\",\n    \"mobx-react\": \"^6.1.4\",\n    \"qrcode\": \"^1.4.1\",\n    \"qs\": \"^6.7.0\",\n    \"query-string\": \"7\",\n    \"raw-loader\": \"^4.0.2\",\n    \"react\": \"^17.0.1\",\n    \"react-dom\": \"^17.0.1\",\n    \"react-intl\": \"^3.9.2\",\n    \"react-markdown\": \"^6.0.2\",\n    \"redux\": \"4.2.1\",\n    \"redux-saga\": \"^0.16.2\",\n    \"reflect-metadata\": \"^0.1.13\",\n    \"regenerator-runtime\": \"^0.13.3\",\n    \"remark\": \"^11.0.2\",\n    \"short-uuid\": \"^3.1.1\",\n    \"showdown\": \"^1.9.0\",\n    \"tldjs\": \"^2.3.1\",\n    \"turndown\": \"5.0.3\",\n    \"typedi\": \"^0.8.0\",\n    \"umi-request\": \"^1.2.15\",\n    \"webdav\": \"^5.2.2\"\n  },\n  \"devDependencies\": {\n    \"@types/chrome\": \"^0.0.268\",\n    \"@types/classnames\": \"^2.2.9\",\n    \"@types/codemirror\": \"^0.0.76\",\n    \"@types/history\": \"^4.7.2\",\n    \"@types/jquery\": \"^3.3.6\",\n    \"@types/lodash\": \"^4.14.161\",\n    \"@types/qrcode\": \"^1.3.3\",\n    \"@types/qs\": \"^6.9.0\",\n    \"@types/react\": \"^17.0.6\",\n    \"@types/react-dom\": \"^16.9.9\",\n    \"@types/react-redux\": \"^7.0.8\",\n    \"@types/react-router\": \"^5.1.3\",\n    \"@types/showdown\": \"^1.9.3\",\n    \"@types/tldjs\": \"^2.3.0\",\n    \"@types/yargs\": \"^17.0.2\",\n    \"@vitest/coverage-v8\": \"^0.32.2\",\n    \"axios\": \"^0.21.1\",\n    \"clean-webpack-plugin\": \"^0.1.19\",\n    \"compressing\": \"^1.4.0\",\n    \"copy-webpack-plugin\": \"^5.1.1\",\n    \"css-loader\": \"^1.0.0\",\n    \"html-webpack-plugin\": \"^3.2.0\",\n    \"less\": \"^3.8.1\",\n    \"less-loader\": \"^7.0.2\",\n    \"prettier\": \"^3.3.2\",\n    \"pump\": \"^3.0.0\",\n    \"style-loader\": \"^0.23.1\",\n    \"terser-webpack-plugin\": \"^2.3.1\",\n    \"ts-loader\": \"^6.2.1\",\n    \"ts-node\": \"^10.9.2\",\n    \"typescript\": \"^5.1.6\",\n    \"url-loader\": \"^3.0.0\",\n    \"vitest\": \"^0.32.2\",\n    \"webpack\": \"^4.41.5\",\n    \"webpack-cli\": \"^3.3.2\",\n    \"webpack-merge\": \"^4.2.2\",\n    \"yargs\": \"^17.1.1\"\n  },\n  \"packageManager\": \"pnpm@8.14.0+sha512.5d4bf97b349faf1a51318aa1ba887e99d9c36e203dbcb55938a91fddd2454246cb00723d6642f54d463a0f52a2701dadf8de002a37fc613c9cdc94ed5675ddce\"\n}\n"
  },
  {
    "path": "script/build.js",
    "content": "const webpack = require('webpack');\nconst prodConfig = require('../webpack/webpack.prod');\nconst compiler = webpack(prodConfig);\n\nfunction send(data) {\n  if (!process.send) {\n    return;\n  }\n  return new Promise((r) => {\n    process.send(data, null, {}, r);\n  });\n}\n\ncompiler.run((err) => {\n  if (err) {\n    console.log(err);\n  }\n  send({\n    type: 'Success',\n  });\n});\n"
  },
  {
    "path": "script/release.ts",
    "content": "import { fork } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\nimport { pack } from './utils/pack';\n\n(async () => {\n  const releaseDir = path.join(__dirname, '../release');\n  if (!fs.existsSync(releaseDir)) {\n    fs.mkdirSync(releaseDir);\n  }\n  await build();\n  await pack({\n    releaseDir,\n    distDir: path.join(__dirname, '../dist/chrome'),\n    fileName: 'web-clipper-chrome.zip',\n  });\n  await pack({\n    releaseDir,\n    distDir: path.join(__dirname, '../dist'),\n    fileName: 'web-clipper-firefox.zip',\n  });\n  const manifestConfig = path.join(__dirname, '../dist/manifest.json');\n  const content = fs.readFileSync(manifestConfig, 'utf-8');\n  const manifest = JSON.parse(content);\n  manifest.browser_specific_settings = {\n    gecko: {\n      id: '{3fbb1f97-0acf-49a0-8348-36e91bef22ea}',\n    },\n  };\n  manifest.name = 'Universal Web Clipper';\n  fs.writeFileSync(manifestConfig, JSON.stringify(manifest, null, 2));\n  await pack({\n    releaseDir,\n    distDir: path.join(__dirname, '../dist'),\n    fileName: 'web-clipper-firefox-store.zip',\n  });\n})();\n\nfunction build() {\n  const buildScript = require.resolve('./build');\n  const buildEnv = Object.create(process.env);\n  buildEnv.NODE_ENV = 'production';\n  const cp = fork(buildScript, [], {\n    env: buildEnv as unknown as typeof process.env,\n    stdio: 'inherit',\n  });\n  return new Promise((r) => {\n    cp.on('message', r);\n  });\n}\n"
  },
  {
    "path": "script/utils/pack.ts",
    "content": "import compressing from 'compressing';\nimport fs from 'fs';\nimport path from 'path';\nconst pump = require('pump');\ninterface IPackOptions {\n  distDir: string;\n  releaseDir: string;\n  fileName: string;\n}\n\nexport function pack(options: IPackOptions) {\n  const zipStream = new compressing.zip.Stream();\n  const files = fs.readdirSync(options.distDir).filter((p) => !p.match(/^\\./));\n  for (const file of files) {\n    zipStream.addEntry(path.join(options.distDir, file));\n  }\n  const dest = path.join(options.releaseDir, options.fileName);\n  const destStream = fs.createWriteStream(dest);\n  return new Promise((r) => {\n    pump(zipStream, destStream, r);\n  });\n}\n"
  },
  {
    "path": "src/__test__/utils.ts",
    "content": "import { IRequestService, TRequestOption } from '@/service/common/request';\nimport { vi, Mock } from 'vitest';\n\ntype TMockRequestServiceHandler = (url: string, options?: TRequestOption) => any;\n\nexport class MockRequestService implements IRequestService {\n  public mock: {\n    request: Mock;\n  };\n  private handler: TMockRequestServiceHandler;\n  constructor(handler: TMockRequestServiceHandler) {\n    this.mock = {\n      request: vi.fn(),\n    };\n    this.handler = handler;\n  }\n\n  request(url: string, options: TRequestOption) {\n    this.mock.request(url, options);\n    return this.handler(url, options);\n  }\n\n  download(url: string): Promise<Blob> {\n    return Promise.resolve(new Blob([url]));\n  }\n}\n"
  },
  {
    "path": "src/actions/account.ts",
    "content": "import { UserInfo } from '@/common/backend/services/interface';\nimport { AccountPreference } from '@/common/types';\nimport { actionCreatorFactory } from 'dva-model-creator';\n\nconst actionCreator = actionCreatorFactory('account');\n\nexport const asyncAddAccount = actionCreator.async<\n  {\n    id: string;\n    info: any;\n    imageHosting?: string;\n    defaultRepositoryId?: string;\n    userInfo: UserInfo;\n    type: string;\n    callback(): void;\n  },\n  {\n    accounts: AccountPreference[];\n    defaultAccountId: string;\n  },\n  void\n>('asyncAddAccount');\n\nexport const initAccounts = actionCreator.async<\n  void,\n  { accounts: AccountPreference[]; defaultAccountId: string }\n>('initAccounts');\n\nexport const asyncDeleteAccount = actionCreator.async<\n  { id: string },\n  {\n    accounts: AccountPreference[];\n    defaultAccountId: string;\n  },\n  void\n>('asyncDeleteAccount');\n\nexport const asyncUpdateDefaultAccountId = actionCreator.async<{ id?: string }, void>(\n  'asyncUpdateDefaultAccountId'\n);\n\nexport const asyncUpdateAccount = actionCreator<{\n  id: string;\n  account: {\n    info: any;\n    imageHosting?: string;\n    defaultRepositoryId?: string;\n    type: string;\n  };\n  newId: string;\n  userInfo: UserInfo;\n  callback(): void;\n}>('asyncUpdateAccount');\n"
  },
  {
    "path": "src/actions/clipper.ts",
    "content": "import { ClipperHeaderForm } from 'common/modelTypes/clipper';\nimport { Repository, CompleteStatus, CreateDocumentRequest } from 'common/backend/index';\nimport { actionCreatorFactory } from 'dva-model-creator';\n\nconst actionCreator = actionCreatorFactory('clipper');\n\nexport const asyncCreateDocument = actionCreator.async<\n  { pathname: string },\n  {\n    result: CompleteStatus;\n    request: CreateDocumentRequest;\n  },\n  null\n>('ASYNC_CREATE_DOCUMENT');\n\nexport const asyncUploadImage = actionCreator.async<void, void, void>('ASYNC_UPLOAD_IMAGE');\n\nexport const selectRepository = actionCreator<{ repositoryId: string }>('SELECT_REPOSITORY');\n\nexport const initTabInfo = actionCreator<{ title: string; url: string }>('INIT_TAB_INFO');\n\nexport const asyncChangeAccount = actionCreator.async<\n  {\n    id: string;\n  },\n  {\n    repositories: Repository[];\n    currentImageHostingService?: { type: string };\n  },\n  any\n>('ASYNC_CHANGE_ACCOUNT');\n\nexport const changeData = actionCreator<{ data: any; pathName: string }>('CHANGE_DATA');\n\nexport const watchActionChannel = actionCreator('watchActionChannel');\n\nexport const updateClipperHeader = actionCreator<ClipperHeaderForm>('updateClipperHeader');\n"
  },
  {
    "path": "src/actions/userPreference.ts",
    "content": "import { IExtensionWithId } from './../extensions/common';\nimport { ImageHosting, GlobalStore } from '@/common/types';\nimport { PreferenceStorage } from 'common/storage/interface';\nimport { actionCreatorFactory } from 'dva-model-creator';\n\nconst actionCreator = actionCreatorFactory('userPreference');\n\nexport const initUserPreference = actionCreator<PreferenceStorage>('INIT_USER_PREFERENCE');\n\nexport const asyncChangeDefaultRepository = actionCreator.async<\n  {\n    defaultRepositoryId: string;\n  },\n  void\n>('ASYNC_CHANGE_DEFAULT_REPOSITORY');\n\nexport const asyncSetEditorLiveRendering = actionCreator.async<\n  {\n    value: boolean;\n  },\n  {\n    value: boolean;\n  },\n  void\n>('ASYNC_SET_EDITOR_LIVE_RENDERING');\n\nexport const asyncSetIconColor = actionCreator.async<\n  {\n    value: 'dark' | 'light' | 'auto';\n  },\n  {\n    value: 'dark' | 'light' | 'auto';\n  },\n  void\n>('ASYNC_SET_ICON_COLOR');\n\nexport const asyncRunExtension = actionCreator.async<\n  {\n    pathname: string;\n    extension: IExtensionWithId;\n  },\n  {\n    result: unknown;\n    pathname: string;\n  },\n  void\n>('ASYNC_RUN_EXTENSION');\n\nexport const asyncRunScript = actionCreator.async<string, void, void>('ASYNC_RUN_SCRIPT');\n\nexport const asyncAddImageHosting = actionCreator.async<\n  { closeModal: () => void } & Omit<ImageHosting, 'id'>,\n  ImageHosting[],\n  void\n>('ASYNC_ADD_IMAGE_HOSTING');\n\nexport const asyncDeleteImageHosting = actionCreator.async<{ id: string }, ImageHosting[], void>(\n  'ASYNC_DELETE_IMAGE_HOSTING'\n);\n\nexport const asyncEditImageHosting = actionCreator.async<\n  { id: string; value: Omit<ImageHosting, 'id'>; closeModal: () => void },\n  ImageHosting[],\n  void\n>('ASYNC_EDIT_IMAGE_HOSTING');\n\nexport const setLocale = actionCreator<string>('setLocale');\nexport const asyncSetLocaleToStorage = actionCreator<string>('asyncSetLocaleToStorage');\n\nexport const initServices = actionCreator<\n  Pick<GlobalStore['userPreference'], 'servicesMeta' | 'imageHostingServicesMeta'>\n>('initServices');\n"
  },
  {
    "path": "src/common/backend/clients/github/client.test.ts",
    "content": "import { MockRequestService } from '@/__test__/utils';\nimport { GithubClient } from './client';\nimport { IRepository } from './types';\n\ndescribe('test GithubClient', () => {\n  test('test generateNewTokenUrl', () => {\n    expect(GithubClient.generateNewTokenUrl).toEqual(\n      'https://github.com/settings/tokens/new?scopes=repo&description=Web%20Clipper'\n    );\n  });\n\n  function getTestFixtures(response?: any) {\n    let handler = () => response;\n    if (typeof response === 'function') {\n      handler = response;\n    }\n    const mockRequestService = new MockRequestService(handler);\n    const githubClient = new GithubClient({\n      token: 'DiamondYuan',\n      request: mockRequestService,\n    });\n    return {\n      mockRequestService: mockRequestService.mock.request,\n      githubClient,\n    };\n  }\n\n  test('test getUserInfo', async () => {\n    const expectResult = { login: 'DiamondYuan' };\n    const testFixtures = getTestFixtures(expectResult);\n    const result = await testFixtures.githubClient.getUserInfo();\n    expect(testFixtures.mockRequestService.mock.calls[0]).toEqual([\n      'https://api.github.com/user',\n      {\n        headers: {\n          Accept: 'application/vnd.github.v3+json',\n          Authorization: 'token DiamondYuan',\n        },\n        method: 'get',\n      },\n    ]);\n    expect(result).toEqual(expectResult);\n  });\n\n  test('test createIssue', async () => {\n    const expectResult = { html_url: 'html_url', id: 'id' };\n    const testFixtures = getTestFixtures(expectResult);\n    const result = await testFixtures.githubClient.createIssue({\n      title: 'title',\n      body: 'body',\n      labels: ['label1', 'label2'],\n      namespace: 'webclipper/web-clipper',\n    });\n    expect(testFixtures.mockRequestService.mock.calls[0]).toEqual([\n      'https://api.github.com/repos/webclipper/web-clipper/issues',\n      {\n        data: { title: 'title', body: 'body', labels: ['label1', 'label2'] },\n        method: 'post',\n        requestType: 'json',\n        headers: {\n          Accept: 'application/vnd.github.v3+json',\n          Authorization: 'token DiamondYuan',\n        },\n      },\n    ]);\n    expect(result).toEqual(expectResult);\n  });\n\n  test('test createIssue', async () => {\n    const expectResult = { html_url: 'html_url', id: 'id' };\n    const testFixtures = getTestFixtures(expectResult);\n    const result = await testFixtures.githubClient.uploadFile({\n      owner: 'owner',\n      repo: 'repo',\n      path: 'path',\n      message: 'message',\n      content: 'content',\n      branch: 'branch',\n    });\n    expect(testFixtures.mockRequestService.mock.calls[0]).toEqual([\n      'https://api.github.com/repos/owner/repo/contents/path',\n      {\n        data: { message: 'message', content: 'content', branch: 'branch' },\n        method: 'put',\n        headers: {\n          Accept: 'application/vnd.github.v3+json',\n          Authorization: 'token DiamondYuan',\n        },\n      },\n    ]);\n    expect(result).toEqual(expectResult);\n  });\n\n  test('test list branches', async () => {\n    const testFixtures = getTestFixtures([]);\n    await testFixtures.githubClient.listBranch({\n      owner: 'owner',\n      repo: 'repo',\n      protected: false,\n      page: 1,\n      per_page: 100,\n    });\n    expect(testFixtures.mockRequestService.mock.calls[0]).toEqual([\n      'https://api.github.com/repos/owner/repo/branches?protected=false&per_page=100&page=1',\n      {\n        method: 'get',\n        headers: {\n          Accept: 'application/vnd.github.v3+json',\n          Authorization: 'token DiamondYuan',\n        },\n      },\n    ]);\n  });\n\n  test('test getRepos', async () => {\n    const testFixtures = getTestFixtures([]);\n    await testFixtures.githubClient.getRepos({\n      visibility: 'all',\n      affiliation: 'owner',\n      page: 1,\n      per_page: 100,\n    });\n    expect(testFixtures.mockRequestService.mock.calls[0]).toEqual([\n      'https://api.github.com/user/repos?affiliation=owner&per_page=100&page=1',\n      {\n        method: 'get',\n        headers: {\n          Accept: 'application/vnd.github.v3+json',\n          Authorization: 'token DiamondYuan',\n        },\n      },\n    ]);\n  });\n\n  test('test git all', async () => {\n    const result: IRepository[] = [];\n    for (let i = 0; i < 670; i++) {\n      result.push({\n        name: `${i}`,\n        full_name: `webclipper/${i}`,\n        default_branch: 'main',\n      });\n    }\n    const testFixtures = getTestFixtures((...args: [string, Object]) => {\n      const url = new URL(args[0]);\n      const page = parseInt(url.searchParams.get('page')!, 10);\n      const per_page = parseInt(url.searchParams.get('per_page')!, 10);\n      return result.slice(per_page * (page - 1), per_page * page);\n    });\n    const res = await testFixtures.githubClient.queryAll(\n      {\n        visibility: 'all',\n        affiliation: 'owner',\n      },\n      testFixtures.githubClient.getRepos\n    );\n    expect(res).toEqual(result);\n  });\n});\n"
  },
  {
    "path": "src/common/backend/clients/github/client.ts",
    "content": "import { IExtendRequestHelper } from '@/service/common/request';\nimport { RequestHelper } from '@/service/request/common/request';\nimport { stringify } from 'qs';\nimport {\n  ICreateIssueOptions,\n  ICreateIssueResponse,\n  IGithubClientOptions,\n  IGithubUserInfoResponse,\n  IUploadFileOptions,\n  IUploadFileResponse,\n  IListBranchesOptions,\n  IBranch,\n  TPageRequest,\n  IPageQuery,\n  TOmitPage,\n  IGetGithubRepositoryOptions,\n  IRepository,\n} from './types';\n\nexport class GithubClient {\n  private options: IGithubClientOptions;\n  private request: IExtendRequestHelper;\n\n  constructor(options: IGithubClientOptions) {\n    this.options = options;\n    this.request = new RequestHelper({\n      baseURL: 'https://api.github.com/',\n      headers: {\n        Accept: 'application/vnd.github.v3+json',\n        Authorization: `token ${this.options.token}`,\n      },\n      request: this.options.request,\n    });\n  }\n\n  createIssue = async (options: ICreateIssueOptions) => {\n    const data = { title: options.title, body: options.body, labels: options.labels };\n    const response = await this.request.post<ICreateIssueResponse>(\n      `repos/${options.namespace}/issues`,\n      { data }\n    );\n    return response;\n  };\n\n  getUserInfo = () => {\n    return this.request.get<IGithubUserInfoResponse>('user');\n  };\n\n  queryAll = async <O extends IPageQuery, T>(\n    args: TOmitPage<O>,\n    pageRequest: TPageRequest<O, T>\n  ): Promise<T[]> => {\n    const startPage: number = 1;\n    const pageSize: number = 50;\n    const baseArgs = { ...args, page: startPage, per_page: pageSize } as O;\n    let result: T[] = [];\n    while (true) {\n      const response = await pageRequest(baseArgs);\n      result = result.concat(response);\n      if (response.length === pageSize) {\n        baseArgs.page = baseArgs.page + 1;\n        continue;\n      }\n      return result;\n    }\n  };\n\n  /**\n   * Create or update file contents\n   *\n   * @see https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#create-or-update-file-contents\n   */\n  uploadFile = (options: IUploadFileOptions) => {\n    return this.request.put<IUploadFileResponse>(\n      `repos/${options.owner}/${options.repo}/contents/${options.path}`,\n      {\n        data: {\n          message: options.message,\n          content: options.content,\n          branch: options.branch,\n        },\n      }\n    );\n  };\n\n  listBranch = (options: IListBranchesOptions) => {\n    return this.request.get<IBranch[]>(\n      `repos/${options.owner}/${options.repo}/branches?${stringify({\n        protected: options.protected,\n        per_page: options.per_page,\n        page: options.page,\n      })}`\n    );\n  };\n\n  /**\n   *\n   * @see https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#list-repositories-for-the-authenticated-user\n   * @param options IGetGithubRepositoryOptions\n   */\n  getRepos = (options: IGetGithubRepositoryOptions) => {\n    return this.request.get<IRepository[]>(\n      `user/repos?${stringify({\n        affiliation: options.affiliation,\n        per_page: options.per_page,\n        type: options.type,\n        page: options.page,\n      })}`\n    );\n  };\n\n  static get generateNewTokenUrl() {\n    return `https://github.com/settings/tokens/new?${stringify({\n      scopes: 'repo',\n      description: 'Web Clipper',\n    })}`;\n  }\n}\n"
  },
  {
    "path": "src/common/backend/clients/github/types.ts",
    "content": "import { IRequestService } from '@/service/common/request';\n\nexport interface IGithubClientOptions {\n  token: string;\n  request: IRequestService;\n}\nexport interface ICreateIssueOptions {\n  title: string;\n  body: string;\n  labels: string[];\n  namespace: string;\n}\n\nexport interface ICreateIssueResponse {\n  html_url: string;\n  id: number;\n}\n\nexport interface IGithubUserInfoResponse {\n  login: string;\n  avatar_url: string;\n  name: string;\n  bio: string;\n  html_url: string;\n}\n\nexport interface IUploadFileOptions {\n  owner: string;\n  repo: string;\n  path: string;\n  message: string;\n  content: string;\n  branch?: string;\n}\n\nexport interface IUploadFileResponse {\n  content: {\n    html_url: string;\n  };\n}\n\nexport interface IListBranchesOptions extends IPageQuery {\n  owner: string;\n  repo: string;\n  protected: boolean;\n}\n\nexport interface IBranch {\n  name: string;\n  protected: boolean;\n}\n\nexport interface IPageQuery {\n  per_page: number;\n  page: number;\n}\n\nexport type TOmitPage<T> = Omit<T, 'page' | 'per_page'>;\n\nexport type TPageRequest<O extends IPageQuery, R> = (option: O) => Promise<R[]>;\n\nexport interface IGetGithubRepositoryOptions extends IPageQuery {\n  visibility?: 'all' | 'public' | 'private';\n  affiliation?: 'owner' | 'collaborator' | 'organization_member';\n  type?: 'all' | 'owner' | 'public' | 'private' | 'member';\n}\n\nexport interface IRepository {\n  name: string;\n  /**\n   *like webclipper/web-clipper\n   */\n  full_name: string;\n  default_branch: string;\n}\n"
  },
  {
    "path": "src/common/backend/clients/joplin/LegacyJoplinClient.ts",
    "content": "import { Repository } from './../../services/interface';\nimport { JoplinFolderItem, JoplinTag, JoplinCreateDocumentRequest } from './types';\nimport { AbstractJoplinClient } from './basic';\n\nexport class LegacyJoplinClient extends AbstractJoplinClient {\n  getRepositories = async () => {\n    const repositories: Repository[] = [];\n    const folders = await this.request.get<JoplinFolderItem[]>('folders');\n    folders.forEach(folder => {\n      repositories.push({\n        id: folder.id,\n        name: folder.title,\n        groupId: folder.id,\n        groupName: folder.title,\n      });\n      if (Array.isArray(folder.children)) {\n        folder.children.forEach(subFolder => {\n          repositories.push({\n            id: subFolder.id,\n            name: subFolder.title,\n            groupId: folder.id,\n            groupName: folder.title,\n          });\n        });\n      }\n    });\n    return repositories;\n  };\n\n  getTags = async (filterTags: boolean) => {\n    let tags = await this.request.get<JoplinTag[]>('tags');\n    if (filterTags) {\n      tags = (\n        await Promise.all(\n          tags.map(async tag => {\n            console.log(this);\n            const notes = await this.request.get<unknown[]>(`tags/${tag.id}/notes`);\n            if (notes.length === 0) {\n              return null;\n            }\n            return tag;\n          })\n        )\n      ).filter((tag): tag is JoplinTag => !!tag);\n    }\n    return tags;\n  };\n\n  createDocument = async (data: JoplinCreateDocumentRequest) => {\n    await this.request.post('notes', {\n      data: {\n        parent_id: data.repositoryId,\n        title: data.title,\n        body: data.content,\n        tags: data.tags.join(','),\n        source_url: data.url,\n      },\n    });\n  };\n}\n"
  },
  {
    "path": "src/common/backend/clients/joplin/basic.ts",
    "content": "import { generateUuid } from '@web-clipper/shared/lib/uuid';\nimport { IExtendRequestHelper } from '@/service/common/request';\nimport { Repository, IJoplinClient, JoplinTag, JoplinCreateDocumentRequest } from './types';\n\nexport abstract class AbstractJoplinClient implements IJoplinClient {\n  constructor(protected request: IExtendRequestHelper) {}\n\n  public uploadBlob = async (blob: Blob): Promise<string> => {\n    let formData = new FormData();\n    formData.append('data', blob);\n    formData.append(\n      'props',\n      JSON.stringify({\n        title: generateUuid(),\n      })\n    );\n    const result = await this.request.postForm<{ id: string }>(`resources`, {\n      data: formData,\n    });\n    return `:/${result.id}`;\n  };\n\n  abstract getTags(filterTags: boolean): Promise<JoplinTag[]>;\n  abstract getRepositories(): Promise<Repository[]>;\n  abstract createDocument(data: JoplinCreateDocumentRequest): Promise<void>;\n}\n"
  },
  {
    "path": "src/common/backend/clients/joplin/client.ts",
    "content": "import { AbstractJoplinClient } from './basic';\nimport {\n  Repository,\n  JoplinFolderItem,\n  JoplinTag,\n  JoplinCreateDocumentRequest,\n  IPageRes,\n} from './types';\n\nexport class JoplinClient extends AbstractJoplinClient {\n  support = async (): Promise<boolean> => {\n    let tags = await this.request.get<IPageRes<JoplinTag>>('tags');\n    return typeof tags.has_more === 'boolean';\n  };\n\n  getRepositories = async () => {\n    const repositories: Repository[] = [];\n    const folders = await this.pageToAllList(this.getFolderByPageNumber);\n    folders.forEach(folder => {\n      repositories.push({\n        id: folder.id,\n        name: folder.title,\n        groupId: folder.id,\n        groupName: folder.title,\n      });\n      if (Array.isArray(folder.children)) {\n        folder.children.forEach(subFolder => {\n          repositories.push({\n            id: subFolder.id,\n            name: subFolder.title,\n            groupId: folder.id,\n            groupName: folder.title,\n          });\n        });\n      }\n    });\n    return repositories;\n  };\n\n  getTags = async (filterTags: boolean) => {\n    let tags = await this.pageToAllList<JoplinTag>(this.getTagsByPageNumber);\n    if (filterTags) {\n      tags = (\n        await Promise.all(\n          tags.map(async tag => {\n            const notes = await this.request.get<unknown[]>(`tags/${tag.id}/notes`);\n            console.log('notes', notes);\n            if (notes.length === 0) {\n              return null;\n            }\n            return tag;\n          })\n        )\n      ).filter((tag): tag is JoplinTag => !!tag);\n    }\n    return tags;\n  };\n\n  createDocument = async (data: JoplinCreateDocumentRequest) => {\n    await this.request.post('notes', {\n      data: {\n        parent_id: data.repositoryId,\n        title: data.title,\n        body: data.content,\n        tags: data.tags.join(','),\n        source_url: data.url,\n      },\n    });\n  };\n\n  private getTagsByPageNumber = async (page: number) => {\n    return this.request.get<IPageRes<JoplinTag>>(`tags?page=${page}`);\n  };\n\n  private getFolderByPageNumber = async (page: number) => {\n    return this.request.get<IPageRes<JoplinFolderItem>>(`folders?page=${page}`);\n  };\n\n  private pageToAllList = async <T>(\n    getOnePage: (page: number) => Promise<IPageRes<T>>\n  ): Promise<T[]> => {\n    let hasMore = true;\n    let startPageNumber = 1;\n    let result: T[] = [];\n    while (hasMore) {\n      const response = await getOnePage(startPageNumber);\n      result = result.concat(response.items);\n      hasMore = response.has_more;\n      startPageNumber++;\n    }\n    return result;\n  };\n}\n"
  },
  {
    "path": "src/common/backend/clients/joplin/index.ts",
    "content": "export { JoplinClient } from './client';\nexport { LegacyJoplinClient } from './LegacyJoplinClient';\nexport * from './types';\n"
  },
  {
    "path": "src/common/backend/clients/joplin/types.ts",
    "content": "import type { Repository, CreateDocumentRequest } from '../../services/interface';\n\nexport type { Repository } from '../../services/interface';\nexport type { CreateDocumentRequest } from '../../services/interface';\n\nexport interface IJoplinClient {\n  getTags(filterTags: boolean): Promise<JoplinTag[]>;\n  getRepositories(): Promise<Repository[]>;\n  createDocument(data: JoplinCreateDocumentRequest): Promise<void>;\n  uploadBlob(blob: Blob): Promise<string>;\n}\n\nexport interface JoplinTag {\n  id: string;\n  title: string;\n}\n\nexport interface JoplinCreateDocumentRequest extends CreateDocumentRequest {\n  tags: string[];\n}\n\nexport interface JoplinBackendServiceConfig {\n  token: string;\n  filterTags: boolean;\n}\n\nexport interface JoplinFolderItem {\n  id: string;\n  title: string;\n  children: JoplinFolderItem[];\n}\n\nexport interface JoplinTag {\n  id: string;\n  title: string;\n}\n\nexport interface IJoplinClient {\n  getTags(filterTags: boolean): Promise<JoplinTag[]>;\n  getRepositories(): Promise<Repository[]>;\n  createDocument(data: JoplinCreateDocumentRequest): Promise<void>;\n}\n\nexport interface JoplinCreateDocumentRequest extends CreateDocumentRequest {\n  tags: string[];\n}\n\nexport interface IPageRes<T> {\n  has_more: boolean;\n  items: T[];\n}\n"
  },
  {
    "path": "src/common/backend/clients/leanote/client.test.ts",
    "content": "import { MockRequestService } from '@/__test__/utils';\nimport LeanoteClient from './client';\nconst FormData = require('form-data');\n\ndescribe('test LeanoteClient', () => {\n  test('test login', async () => {\n    const expectedResponseToken = { Token: 'SomeToken' };\n    const mockRequestService = new MockRequestService(() => expectedResponseToken);\n    const inputStub = {\n      leanote_host: 'https://localhost',\n      email: 'email',\n      pwd: 'pwd',\n      token_cached: 'OldToken',\n    };\n    const client = new LeanoteClient(inputStub, mockRequestService);\n    const result = await client.login();\n    const expectUrlGenerated = `${inputStub.leanote_host}/api/auth/login`;\n    expect(mockRequestService.mock.request.mock.calls[0][0]).toEqual(expectUrlGenerated);\n    //TODO add test\n    expect(result).toEqual(expectedResponseToken.Token);\n  });\n\n  test('test repository', async () => {\n    const notebookListStubResponse = [{ NotebookId: 1, Title: 'some_notebook_title' }];\n    const inputStub = {\n      leanote_host: 'https://localhost',\n      email: '',\n      pwd: '',\n      token_cached: 'some_token_when_already_logged',\n    };\n    const mockRequestService = new MockRequestService(() => notebookListStubResponse);\n    const client = new LeanoteClient(inputStub, mockRequestService);\n    const result = await client.getSyncNotebooks();\n    const expectUrlGenerated = `${inputStub.leanote_host}/api/notebook/getSyncNotebooks?token=${inputStub.token_cached}`;\n    expect(mockRequestService.mock.request.mock.calls[0][0]).toEqual(expectUrlGenerated);\n    expect(notebookListStubResponse[0].Title).toEqual(result[0].Title);\n  });\n\n  test('test create document', async () => {\n    const inputStub = {\n      leanote_host: 'https://localhost',\n      email: '',\n      pwd: '',\n      token_cached: 'some_token_when_already_logged',\n    };\n    const mockRequestService = new MockRequestService(function() {\n      // No return expected\n    });\n    const client = new LeanoteClient(inputStub, mockRequestService);\n    await client.createDocument({\n      title: 'some_title',\n      content: 'some_content',\n      repositoryId: 'some_repo',\n    });\n    const expectUrlGenerated = `${inputStub.leanote_host}/api/note/addNote?token=${inputStub.token_cached}`;\n    const calledUrl = mockRequestService.mock.request.mock.calls[0][0];\n    const callRequest = mockRequestService.mock.request.mock.calls[0][1];\n    expect(calledUrl).toEqual(expectUrlGenerated);\n    expect(Object.keys(callRequest.data).length).toBeGreaterThan(0);\n    expect(callRequest.data.constructor).toEqual(FormData);\n    expect(callRequest.requestType).toEqual('form');\n    expect(callRequest.method).toEqual('post');\n  });\n});\n"
  },
  {
    "path": "src/common/backend/clients/leanote/client.ts",
    "content": "import { IExtendRequestHelper, IRequestService } from '@/service/common/request';\nimport { CreateDocumentRequest } from '../../index';\nimport { RequestHelper } from '@/service/request/common/request';\nimport showdown from 'showdown';\nimport {\n  LeanoteBackendServiceConfig,\n  LeanoteCreateDocumentResponse,\n  LeanoteNotebook,\n  LeanoteResponse,\n} from './interface';\nconst FormData = require('form-data');\n\nconst converter = new showdown.Converter();\n/**\n * Client for self hosted leanote or leanote.com\n */\nexport default class LeanoteClient {\n  private config: LeanoteBackendServiceConfig;\n  private request: IExtendRequestHelper;\n  private formData: FormData;\n  private imagesCount: number;\n\n  /**\n   * This class wrap a IExtendRequestHelper to perform HTTP request\n   */\n  constructor(\n    { leanote_host, email, pwd, token_cached }: LeanoteBackendServiceConfig,\n    request: IRequestService\n  ) {\n    this.config = { leanote_host, email, pwd, token_cached };\n    this.formData = new FormData();\n    this.imagesCount = 0;\n    this.request = new RequestHelper({\n      baseURL: this.config.leanote_host,\n      request: request,\n    });\n  }\n\n  /**\n   * @TODO: Support markdown\n   *\n   * Perform a POST with document request as formData to leanote server to create a note\n   *\n   * @see documentation https://github.com/leanote/leanote/wiki/leanote-api\n   */\n  createDocument = async (info: CreateDocumentRequest): Promise<LeanoteCreateDocumentResponse> => {\n    this.formData.append('NotebookId', info.repositoryId);\n    this.formData.append('Title', info.title);\n    this.formData.append('Content', converter.makeHtml(info.content));\n    const formData = this.formData;\n    this.formData = new FormData();\n    this.imagesCount = 0;\n    return this.request.postForm<LeanoteCreateDocumentResponse>(\n      `/api/note/addNote?token=${this.config.token_cached}`,\n      {\n        data: formData,\n      }\n    );\n  };\n\n  /**\n   * Append blob accordingly in the formData and guess computed image url\n   *\n   * @see documentation https://github.com/leanote/leanote/wiki/leanote-api\n   */\n  uploadBlob = async (blob: Blob): Promise<string> => {\n    const ext = blob.type.split('/').pop();\n    const filename = `${this.imagesCount}.${ext}`;\n    const localFileId = `${this.imagesCount}`;\n    this.formData.append(`Files[${localFileId}][LocalFileId]`, localFileId);\n    this.formData.append(`Files[${localFileId}][Title]`, filename);\n    this.formData.append(`Files[${localFileId}][Type]`, blob.type);\n    this.formData.append(`Files[${localFileId}][HasBody]`, 'true');\n    this.imagesCount++;\n    this.formData.append(`FileDatas[${localFileId}]`, blob, filename);\n    return `${this.config.leanote_host}/api/file/getImage?fileId=${localFileId}`;\n  };\n\n  /**\n   * Perform a GET in login api\n   *\n   * @see documentation https://github.com/leanote/leanote/wiki/leanote-api\n   */\n  login = async () => {\n    if (!this.config.email || !this.config.pwd || this.config.email === '') {\n      throw new Error('Cannot login');\n    }\n    /**\n     * change: get method=>postForm method\n     * Remark :\n     * The username and password fields need to be placed in the request body\n     * 用户名和密码的字段需要放在请求体\n     */\n\n    let formData = new FormData();\n    formData.append('email', this.config.email);\n    formData.append('pwd', this.config.pwd);\n\n    const data = await this.request.postForm<LeanoteResponse>(`/api/auth/login`, {\n      data: formData,\n    });\n    this.config.token_cached = data.Token;\n    return data.Token;\n  };\n\n  /**\n   * Perform a GET in getSyncNotebooks api to find notebooks\n   * Get notebooks which need be synced\n   * need Params: afterUsn(int, the usn bigger than it is need be synced)\n   *              maxEntry(int) Number returned by getSyncNotebooks api\n   * leanote:\n   *      afterUsn : Default value = 0\n   *      maxEntry : if maxEntry==0;maxEntry=100\n   * @see documentation https://github.com/leanote/leanote/wiki/leanote-api\n   */\n  getSyncNotebooks = async () => {\n    return this.request.get<LeanoteNotebook[]>(\n      `/api/notebook/getSyncNotebooks?token=${this.config.token_cached}`\n    );\n  };\n  /**\n   * Perform a GET in getNotebooks api to find all notebooks\n   *\n   * @see documentation https://github.com/leanote/leanote/wiki/leanote-api\n   */\n  getNotebooks = async () => {\n    return this.request.get<LeanoteNotebook[]>(\n      `/api/notebook/getNotebooks?token=${this.config.token_cached}`\n    );\n  };\n}\n"
  },
  {
    "path": "src/common/backend/clients/leanote/interface.ts",
    "content": "export interface LeanoteBackendServiceConfig {\n  leanote_host: string;\n  email: string;\n  pwd: string;\n  token_cached: string;\n}\n\nexport interface LeanoteResponse {\n  Ok: string;\n  Msg: string;\n  Token: string;\n}\n\nexport interface LeanoteCreateDocumentResponse {\n  NoteId: string;\n}\n\nexport interface LeanoteNotebook {\n  NotebookId: string;\n  Title: string;\n}\n\nexport interface LeanoteNote {\n  NotebookId: string;\n  Title: string;\n  Content: string;\n}\n"
  },
  {
    "path": "src/common/backend/clients/siyuan/client.ts",
    "content": "import { CreateDocumentRequest } from './../../services/interface';\nimport { IExtendRequestHelper } from '@/service/common/request';\nimport { RequestHelper } from '@/service/request/common/request';\nimport {\n  ISiyuanClientOptions,\n  ISiyuanUploadImageResponse,\n  ISiyuanFetchNotesResponse,\n} from './types';\n\nconst SIYUAN_BASE_URL = 'http://127.0.0.1:6806/';\n\nexport class SiYuanClient {\n  private options: ISiyuanClientOptions;\n  private request: IExtendRequestHelper;\n\n  constructor(options: ISiyuanClientOptions) {\n    this.options = options;\n    const headers: Record<string, string> = {};\n    if (options.accessToken) {\n      headers.Authorization = `Token ${options.accessToken}`;\n    }\n    this.request = new RequestHelper({\n      baseURL: SIYUAN_BASE_URL,\n      headers: headers,\n      request: this.options.request,\n    });\n  }\n\n  listNotebooks = async (): Promise<{ id: string; name: string }[]> => {\n    const res = await this.request.post<ISiyuanFetchNotesResponse>(`api/notebook/lsNotebooks`, {\n      data: {},\n    });\n    return (res.data.notebooks ?? res.data.files ?? [])\n      .map(p => {\n        if (typeof p === 'object') {\n          return p;\n        }\n        return {\n          name: p.split('/')[p.split('/').length - 1],\n          id: p.split('/')[p.split('/').length - 1],\n          closed: false,\n        };\n      })\n      .filter(e => {\n        return !e.closed;\n      });\n  };\n\n  createNote = async (data: CreateDocumentRequest) => {\n    const response = await this.request.post<{ code: number; msg: string; data: string }>(\n      `api/filetree/createDocWithMd`,\n      {\n        data: {\n          notebook: data.repositoryId,\n          path: `${data.title}.sy`,\n          markdown: data.content.replaceAll(SIYUAN_BASE_URL, ''),\n        },\n      }\n    );\n    if (response.code !== 0) {\n      throw new Error(response.msg);\n    }\n    return response.data;\n  };\n\n  uploadImage = async (blob: Blob) => {\n    let formData = new FormData();\n    formData.append('assetsDirPath', '/assets/');\n    const fileName = `${Date.now()}.png`;\n    formData.append('file[]', new File([blob], fileName));\n    const response = await this.request.postForm<ISiyuanUploadImageResponse>(`api/asset/upload`, {\n      data: formData,\n    });\n    return `${SIYUAN_BASE_URL}${response.data.succMap[fileName]}`;\n  };\n}\n"
  },
  {
    "path": "src/common/backend/clients/siyuan/types.ts",
    "content": "import { IRequestService } from '@/service/common/request';\n\nexport interface ISiyuanClientOptions {\n  request: IRequestService;\n  accessToken?: string;\n}\n\nexport interface ISiyuanUploadImageResponse {\n  data: {\n    succMap: {\n      [key: string]: string;\n    };\n  };\n}\n\nexport interface ISiyuanFetchNotesResponse {\n  data: {\n    files?: string[] | { name: string; id: string; closed?: boolean }[];\n    notebooks?: string[] | { name: string; id: string; closed?: boolean }[];\n  };\n}\n"
  },
  {
    "path": "src/common/backend/imageHosting/baklib/index.ts",
    "content": "import localeService from '@/common/locales';\nimport { ImageHostingServiceMeta } from '../interface';\nimport Service from './service';\n\nexport default (): ImageHostingServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.imageHosting.baklib.name',\n      defaultMessage: 'Baklib',\n    }),\n    icon: 'baklib',\n    type: 'baklib',\n    service: Service,\n    builtIn: true,\n    builtInRemark: localeService.format({\n      id: 'backend.imageHosting.baklib.builtInRemark',\n      defaultMessage: 'Baklib built in image hosting service.',\n    }),\n  };\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/baklib/service.ts",
    "content": "import { Base64ImageToBlob, BlobToBase64 } from '@/common/blob';\nimport { UploadImageRequest, ImageHostingService } from '../interface';\nimport md5 from '@web-clipper/shared/lib/md5';\nimport { extend, RequestMethod } from 'umi-request';\nimport { BaklibBackendServiceConfig } from '../../services/baklib/interface';\nimport { Repository } from '../../services/interface';\nexport interface YuqueImageHostingOption {\n  access_token: string;\n}\n\nexport default class YuqueImageHostingService implements ImageHostingService {\n  private accessToken: string;\n  private request: RequestMethod;\n  private context?: { currentRepository: Repository };\n\n  constructor({ accessToken }: BaklibBackendServiceConfig) {\n    this.accessToken = accessToken;\n    this.request = extend({\n      prefix: 'https://www.baklib-free.com/api/',\n      headers: { Authorization: `Bearer ${accessToken}` },\n      timeout: 5000,\n    });\n    this.request.interceptors.response.use(\n      async response => {\n        const json = await response.clone().json();\n        if (json.code !== 0) {\n          throw new Error(json.message || json.error);\n        }\n        return response;\n      },\n      { global: false }\n    );\n  }\n\n  getId() {\n    return md5(this.accessToken);\n  }\n\n  uploadImage = async ({ data }: UploadImageRequest) => {\n    const blob = Base64ImageToBlob(data);\n    return this.uploadBlob(blob);\n  };\n\n  uploadImageUrl = async (url: string) => {\n    const res = await extend({}).get(url, {\n      responseType: 'blob',\n    });\n    let blob: Blob = res;\n    if (blob.type === 'image/webp') {\n      blob = blob.slice(0, blob.size, 'image/jpeg');\n    }\n    return this.uploadBlob(blob);\n  };\n\n  updateContext = (context: { currentRepository: Repository }) => {\n    this.context = context;\n  };\n\n  private uploadBlob = async (blob: Blob): Promise<string> => {\n    if (!this.context?.currentRepository.id) {\n      throw new Error('请选择站点');\n    }\n    console.log('this.context?.currentRepository.id', this.context?.currentRepository.id);\n    let formData = new FormData();\n    formData.append('base64', await BlobToBase64(blob));\n    formData.append('tenant_id', this.context?.currentRepository.id);\n    const result = await this.request.post(`v1/image/upload`, {\n      data: formData,\n      requestType: 'form',\n    });\n    return result.message.url;\n  };\n}\n"
  },
  {
    "path": "src/common/backend/imageHosting/github/form.tsx",
    "content": "import React, { Fragment } from 'react';\r\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\r\nimport { Input, Select, Tooltip } from 'antd';\r\nimport { Form } from '@ant-design/compatible';\r\nimport { FormattedMessage } from 'react-intl';\r\nimport locale from '@/common/locales';\r\nimport IconFont from '@/components/IconFont';\r\nimport { GithubClient } from '../../clients/github/client';\r\nimport { IBasicRequestService } from '@/service/common/request';\r\nimport Container from 'typedi';\r\nimport {\r\n  IBranch,\r\n  IGetGithubRepositoryOptions,\r\n  IRepository,\r\n  IListBranchesOptions,\r\n} from '../../clients/github/types';\r\nimport { useFetch } from '@shihengtech/hooks';\r\nimport { GithubImageHostingOption } from './type';\r\n\r\ninterface Props extends FormComponentProps {\r\n  info: GithubImageHostingOption;\r\n}\r\n\r\ninterface IRepositoryState {\r\n  init: boolean;\r\n  repos: IRepository[];\r\n}\r\n\r\nasync function fetchAllRepos(accessToken?: string): Promise<IRepositoryState> {\r\n  if (!accessToken) {\r\n    return {\r\n      init: false,\r\n      repos: [],\r\n    };\r\n  }\r\n  const githubClient = new GithubClient({\r\n    token: accessToken,\r\n    request: Container.get(IBasicRequestService),\r\n  });\r\n  const repos = await githubClient.queryAll<IGetGithubRepositoryOptions, IRepository>(\r\n    { visibility: 'all' },\r\n    githubClient.getRepos\r\n  );\r\n  return {\r\n    init: true,\r\n    repos: repos,\r\n  };\r\n}\r\n\r\ninterface IBranchState {\r\n  init: boolean;\r\n  branches: IBranch[];\r\n  default_branch?: string;\r\n}\r\n\r\ninterface IFetchBranchesOptions {\r\n  accessToken?: string;\r\n  currentRepo?: string;\r\n  repositoryState: IRepositoryState;\r\n}\r\n\r\nasync function fetchBranches(options: IFetchBranchesOptions): Promise<IBranchState> {\r\n  if (!options.accessToken || !options.repositoryState.init || !options.currentRepo) {\r\n    return {\r\n      init: false,\r\n      branches: [],\r\n    };\r\n  }\r\n  const currentRepository = options.repositoryState.repos?.filter(\r\n    o => o.full_name === options.currentRepo\r\n  )[0];\r\n  const githubClient = new GithubClient({\r\n    token: options.accessToken,\r\n    request: Container.get(IBasicRequestService),\r\n  });\r\n  const branches = await githubClient.queryAll<IListBranchesOptions, IBranch>(\r\n    {\r\n      owner: options.currentRepo.split('/')[0],\r\n      repo: options.currentRepo.split('/')[1],\r\n      protected: false,\r\n    },\r\n    githubClient.listBranch\r\n  );\r\n  return {\r\n    init: true,\r\n    branches,\r\n    default_branch: currentRepository.default_branch,\r\n  };\r\n}\r\n\r\nexport default ({ form: { getFieldDecorator }, info, form }: Props) => {\r\n  const initInfo: Partial<Props['info']> = info || {};\r\n  const accessToken = form.getFieldValue('accessToken');\r\n  const { data: reposResult, loading } = useFetch(() => fetchAllRepos(accessToken), [accessToken], {\r\n    auto: true,\r\n    initialState: {\r\n      data: { init: false, repos: [] },\r\n    },\r\n    onError: () => {\r\n      return {\r\n        data: { init: false, repos: [] },\r\n      };\r\n    },\r\n  });\r\n  const currentRepo = form.getFieldValue('repo');\r\n  const { data: branchResponse, loading: branchLoading } = useFetch(\r\n    () =>\r\n      fetchBranches({\r\n        accessToken,\r\n        currentRepo,\r\n        repositoryState: reposResult!,\r\n      }),\r\n    [accessToken, currentRepo, reposResult],\r\n    {\r\n      onError: () => {\r\n        return {\r\n          data: {\r\n            init: false,\r\n            branches: [],\r\n          },\r\n        };\r\n      },\r\n      auto: true,\r\n      initialState: {\r\n        data: {\r\n          init: false,\r\n          branches: [],\r\n        },\r\n      },\r\n    }\r\n  );\r\n\r\n  return (\r\n    <Fragment>\r\n      <Form.Item label={<FormattedMessage id=\"backend.imageHosting.github.form.accessToken\" />}>\r\n        {getFieldDecorator('accessToken', {\r\n          initialValue: initInfo.accessToken,\r\n          rules: [\r\n            {\r\n              required: true,\r\n              message: (\r\n                <FormattedMessage id=\"backend.imageHosting.github.form.accessToken.errorMessage\" />\r\n              ),\r\n            },\r\n          ],\r\n        })(\r\n          <Input\r\n            onChange={() => form.setFields({ repo: null, branch: null })}\r\n            suffix={\r\n              <Tooltip\r\n                title={\r\n                  <span\r\n                    style={{\r\n                      whiteSpace: 'nowrap',\r\n                    }}\r\n                  >\r\n                    {locale.format({\r\n                      id: 'backend.imageHosting.github.form.generateNewToken',\r\n                    })}\r\n                  </span>\r\n                }\r\n              >\r\n                <a\r\n                  href={GithubClient.generateNewTokenUrl}\r\n                  target={GithubClient.generateNewTokenUrl}\r\n                >\r\n                  <IconFont type=\"key\" />\r\n                </a>\r\n              </Tooltip>\r\n            }\r\n          />\r\n        )}\r\n      </Form.Item>\r\n      <Form.Item label={<FormattedMessage id=\"backend.imageHosting.github.form.repo\" />}>\r\n        {getFieldDecorator('repo', {\r\n          initialValue: initInfo.repo,\r\n          rules: [\r\n            {\r\n              required: true,\r\n              message: <FormattedMessage id=\"backend.imageHosting.github.form.repo.errorMessage\" />,\r\n            },\r\n          ],\r\n        })(\r\n          <Select\r\n            showSearch\r\n            optionFilterProp=\"label\"\r\n            onChange={() => form.setFields({ branch: null })}\r\n            disabled={loading || !reposResult?.init}\r\n            loading={loading}\r\n            options={reposResult?.repos?.map(o => {\r\n              return {\r\n                value: o.full_name,\r\n                key: o.full_name,\r\n                label: o.full_name,\r\n              };\r\n            })}\r\n          />\r\n        )}\r\n      </Form.Item>\r\n      <Form.Item\r\n        label={\r\n          <FormattedMessage defaultMessage=\"Branch\" id=\"backend.imageHosting.github.form.branch\" />\r\n        }\r\n      >\r\n        {getFieldDecorator('branch', {\r\n          initialValue: initInfo.branch,\r\n        })(\r\n          <Select\r\n            disabled={loading || !reposResult?.init || !branchResponse?.init}\r\n            placeholder={branchResponse?.default_branch}\r\n            loading={branchLoading}\r\n            options={branchResponse?.branches?.map((o: IBranch) => {\r\n              return {\r\n                value: o.name,\r\n                key: o.name,\r\n              };\r\n            })}\r\n          />\r\n        )}\r\n      </Form.Item>\r\n      <Form.Item\r\n        label={\r\n          <FormattedMessage\r\n            id=\"backend.imageHosting.github.form.savePath\"\r\n            defaultMessage=\"Save Path\"\r\n          />\r\n        }\r\n      >\r\n        {getFieldDecorator('savePath', {\r\n          initialValue: initInfo.savePath,\r\n          rules: [\r\n            {\r\n              required: false,\r\n            },\r\n          ],\r\n        })(<Input />)}\r\n      </Form.Item>\r\n    </Fragment>\r\n  );\r\n};\r\n"
  },
  {
    "path": "src/common/backend/imageHosting/github/index.ts",
    "content": "import { ImageHostingServiceMeta } from '../interface';\r\nimport Service from './service';\r\nimport Form from './form';\r\n\r\nexport default (): ImageHostingServiceMeta => {\r\n  return {\r\n    name: 'Github',\r\n    icon: 'github',\r\n    type: 'github',\r\n    form: Form,\r\n    service: Service,\r\n    permission: {\r\n      origins: ['https://api.github.com/*'],\r\n    },\r\n  };\r\n};\r\n"
  },
  {
    "path": "src/common/backend/imageHosting/github/service.ts",
    "content": "import { generateUuid } from '@web-clipper/shared/lib/uuid';\r\nimport { BlobToBase64 } from '@/common/blob';\r\nimport { UploadImageRequest, ImageHostingService } from '../interface';\r\nimport { isUndefined } from 'lodash';\r\nimport { GithubClient } from '../../clients/github/client';\r\nimport Container from 'typedi';\r\nimport { IBasicRequestService } from '@/service/common/request';\r\nimport { GithubImageHostingOption } from './type';\r\nimport localeService from '@/common/locales';\r\n\r\nexport default class GithubImageHostingService implements ImageHostingService {\r\n  private config: GithubImageHostingOption;\r\n  private date: Date;\r\n  private githubClient: GithubClient;\r\n  constructor(config: GithubImageHostingOption) {\r\n    this.config = config;\r\n    this.date = new Date();\r\n    this.githubClient = new GithubClient({\r\n      token: this.config.accessToken,\r\n      request: Container.get(IBasicRequestService),\r\n    });\r\n  }\r\n\r\n  getId() {\r\n    return this.config.accessToken;\r\n  }\r\n\r\n  uploadImage = async ({ data }: UploadImageRequest) => {\r\n    return this.uploadAsBase64(data);\r\n  };\r\n\r\n  uploadImageUrl = async (url: string) => {\r\n    const imageBlob = await Container.get(IBasicRequestService).download(url);\r\n    const imageBase64 = await BlobToBase64(imageBlob);\r\n    return this.uploadAsBase64(imageBase64);\r\n  };\r\n\r\n  private generateFilename = (data: string): string => {\r\n    const matchedSuffix: any = data.match(/^data:image\\/(.*);base64,/);\r\n    const suffix: string = matchedSuffix[1];\r\n    return `${generateUuid()}\\.${suffix}`;\r\n  };\r\n\r\n  private uploadAsBase64 = async (data: string): Promise<string> => {\r\n    if (!this.config.repo) {\r\n      throw new Error(\r\n        localeService.format({\r\n          id: 'backend.imageHosting.github.repo.errorMessage',\r\n          defaultMessage: 'Please config the github imageHosting again.',\r\n        })\r\n      );\r\n    }\r\n    const [username, repo] = this.config.repo.split('/');\r\n    if (isUndefined(this.config.savePath)) this.config.savePath = '';\r\n    if (this.config.savePath.startsWith('/')) this.config.savePath.substr(1);\r\n    if (!this.config.savePath.endsWith('/') && this.config.savePath.length > 0)\r\n      this.config.savePath += '/';\r\n\r\n    const fileName = this.generateFilename(data);\r\n    const folderName = this.date\r\n      .toLocaleString('chinese', { hour12: false })\r\n      .replace(new RegExp('/', 'g'), '-')\r\n      .replace(new RegExp(':', 'g'), '-');\r\n    const filteredImage = data.replace(/^data:image\\/.*;base64,/, '');\r\n    const response = await this.githubClient.uploadFile({\r\n      owner: username,\r\n      repo,\r\n      branch: this.config.branch,\r\n      path: `${this.config.savePath}${folderName}/${fileName}`,\r\n      message: `Upload image \"${fileName}\"`,\r\n      content: filteredImage,\r\n    });\r\n    return `${response.content.html_url}?raw=true`;\r\n  };\r\n}\r\n"
  },
  {
    "path": "src/common/backend/imageHosting/github/type.ts",
    "content": "export interface GithubImageHostingOption {\n  accessToken: string;\n  repo: string;\n  branch?: string;\n  savePath: string;\n}\n"
  },
  {
    "path": "src/common/backend/imageHosting/imgur/form.tsx",
    "content": "import React from 'react';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\n\ninterface Props extends FormComponentProps {\n  info: {\n    clientId: string;\n  };\n}\n\nexport default ({ form: { getFieldDecorator }, info }: Props) => {\n  const initInfo: Partial<Props['info']> = info || {};\n  return (\n    <Form.Item label=\"ClientId\">\n      {getFieldDecorator('clientId', {\n        initialValue: initInfo.clientId,\n        rules: [\n          {\n            required: true,\n          },\n        ],\n      })(<Input placeholder=\"please input clientId\"></Input>)}\n    </Form.Item>\n  );\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/imgur/index.ts",
    "content": "import Form from './form';\nimport { ImageHostingServiceMeta } from '../interface';\nimport Service from './service';\n\nexport default (): ImageHostingServiceMeta => {\n  return {\n    name: 'Imgur',\n    icon: 'imgur',\n    type: 'imgur',\n    service: Service,\n    form: Form,\n    permission: {\n      origins: ['https://api.imgur.com/*'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/imgur/service.ts",
    "content": "import { IBasicRequestService } from '@/service/common/request';\nimport { RequestHelper } from '@/service/request/common/request';\nimport { UploadImageRequest, ImageHostingService } from '../interface';\nimport { Base64ImageToBlob } from '@/common/blob';\nimport Container from 'typedi';\n\nexport interface ImgurImageHostingOption {\n  clientId: string;\n}\n\nexport default class ImgurImageHostingService implements ImageHostingService {\n  private config: ImgurImageHostingOption;\n\n  constructor(config: ImgurImageHostingOption) {\n    this.config = config;\n  }\n\n  getId = () => {\n    return this.config.clientId;\n  };\n\n  uploadImage = async ({ data }: UploadImageRequest) => {\n    const blob = Base64ImageToBlob(data);\n    return this.uploadBlob(blob);\n  };\n\n  uploadImageUrl = async (url: string) => {\n    return this.uploadBlob(url);\n  };\n\n  private uploadBlob = async (blob: Blob | string): Promise<string> => {\n    let formData = new FormData();\n    formData.append('image', blob);\n    const request = new RequestHelper({ request: Container.get(IBasicRequestService) });\n    const result = await request.postForm<{ data: { link: string } }>(\n      `https://api.imgur.com/3/image`,\n      {\n        data: formData,\n        headers: {\n          Authorization: `Client-ID ${this.config.clientId}`,\n        },\n      }\n    );\n    return result.data.link;\n  };\n}\n"
  },
  {
    "path": "src/common/backend/imageHosting/interface.ts",
    "content": "import { Repository } from '../services/interface';\n\nexport interface ImageHostingServiceConstructAble {\n  new (info: any): ImageHostingService;\n}\n\nexport interface ImageHostingService {\n  getId(): string;\n\n  uploadImage(request: UploadImageRequest): Promise<string>;\n\n  uploadImageUrl(url: string): Promise<string>;\n\n  updateContext?({ currentRepository }: { currentRepository: Repository }): void;\n}\n\nexport interface UploadImageRequest {\n  data: string;\n}\n\nexport interface ImageHostingServiceMeta {\n  name: string;\n  icon: string;\n  type: string;\n  service: ImageHostingServiceConstructAble;\n  form?: any;\n  support?: (type: string) => boolean;\n  builtIn?: boolean;\n  builtInRemark?: string;\n  permission?: chrome.permissions.Permissions;\n}\n\nexport const BUILT_IN_IMAGE_HOSTING_ID = 'BUILT_IN_IMAGE_HOSTING_ID';\n"
  },
  {
    "path": "src/common/backend/imageHosting/joplin/index.ts",
    "content": "import localeService from '@/common/locales';\nimport { ImageHostingServiceMeta } from '../interface';\nimport Service from './service';\n\nexport default (): ImageHostingServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.imageHosting.joplin.name',\n    }),\n    icon: 'joplin',\n    type: 'joplin',\n    service: Service,\n    builtIn: true,\n    builtInRemark: localeService.format({\n      id: 'backend.imageHosting.joplin.builtInRemark',\n      defaultMessage: 'Joplin built in image hosting service.',\n    }),\n  };\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/joplin/service.ts",
    "content": "import { RequestHelper } from '@/service/request/common/request';\nimport { JoplinClient } from './../../clients/joplin/index';\nimport { IJoplinClient } from './../../clients/joplin/types';\nimport { IBasicRequestService } from '@/service/common/request';\nimport { Base64ImageToBlob } from '@/common/blob';\nimport { UploadImageRequest, ImageHostingService } from '../interface';\nimport Container from 'typedi';\n\nexport interface JoplinImageHostingOption {\n  token: string;\n}\n\nexport default class JoplinImageHostingService implements ImageHostingService {\n  private client: IJoplinClient;\n  private token: string;\n\n  constructor({ token }: JoplinImageHostingOption) {\n    this.token = token;\n    const request = new RequestHelper({\n      baseURL: 'http://localhost:41184/',\n      request: Container.get(IBasicRequestService),\n      params: {\n        token: token,\n      },\n    });\n    this.client = new JoplinClient(request);\n  }\n\n  getId() {\n    return this.token;\n  }\n\n  uploadImage = async ({ data }: UploadImageRequest) => {\n    const blob = Base64ImageToBlob(data);\n    return this.client.uploadBlob(blob);\n  };\n\n  uploadImageUrl = async (url: string) => {\n    let blob: Blob = await Container.get(IBasicRequestService).download(url);\n    if (blob.type === 'image/webp') {\n      blob = blob.slice(0, blob.size, 'image/jpeg');\n    }\n    return this.client.uploadBlob(blob);\n  };\n}\n"
  },
  {
    "path": "src/common/backend/imageHosting/leanote/index.ts",
    "content": "import localeService from '@/common/locales';\nimport { ImageHostingServiceMeta } from '../interface';\nimport Service from './service';\n\nexport default (): ImageHostingServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.imageHosting.leanote.name',\n    }),\n    icon: 'leanote',\n    type: 'leanote',\n    service: Service,\n    builtIn: true,\n    builtInRemark: localeService.format({\n      id: 'backend.imageHosting.leanote.builtInRemark',\n    }),\n  };\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/leanote/service.ts",
    "content": "import { IBasicRequestService } from '@/service/common/request';\nimport { Base64ImageToBlob } from '@/common/blob';\nimport { UploadImageRequest, ImageHostingService } from '../interface';\nimport backend from '../..';\nimport { message } from 'antd';\nimport localeService from '@/common/locales';\nimport Container from 'typedi';\n\n/**\n * Use leanote as image hosting service by embbeding images to note body\n */\nexport default class LeanoteImageHostingService implements ImageHostingService {\n  getId = () => {\n    return 'leanote';\n  };\n\n  uploadImage = async ({ data }: UploadImageRequest) => {\n    const blob = Base64ImageToBlob(data);\n    return this.uploadBlob(blob);\n  };\n\n  uploadImageUrl = async (url: string) => {\n    let blob: Blob = await Container.get(IBasicRequestService).download(url);\n    if (blob.type === 'image/webp') {\n      blob = blob.slice(0, blob.size, 'image/jpeg');\n    }\n    return this.uploadBlob(blob);\n  };\n\n  /**\n   * Delegate image saving to document service\n   *\n   * @param blob\n   *\n   * @return string image url once hosted\n   */\n  private uploadBlob = async (blob: Blob): Promise<string> => {\n    message.destroy();\n    message.warning(\n      localeService.format({\n        id: 'backend.services.leanote.warning.image.host.saving.delayed',\n        defaultMessage: 'Image will be attached only if the current clipping is saved',\n      })\n    );\n    return (backend.getDocumentService()! as any).uploadBlob(blob);\n  };\n}\n"
  },
  {
    "path": "src/common/backend/imageHosting/piclist/form.tsx",
    "content": "import React, { Fragment } from 'react';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\n\ninterface Props extends FormComponentProps {\n  info: {\n    uploadUrl: string;\n    key: string;\n  };\n}\n\nexport default ({ form: { getFieldDecorator }, info }: Props) => {\n  const initInfo: Partial<Props['info']> = info || {};\n  return (\n    <Fragment>\n      <Form.Item label=\"UploadUrl\">\n        {getFieldDecorator('uploadUrl', {\n          initialValue: initInfo.uploadUrl,\n          rules: [\n            {\n              required: true,\n            },\n          ],\n        })(<Input placeholder=\"please input piclist upload URL\"></Input>)}\n      </Form.Item>\n      <Form.Item label=\"Key\">\n        {getFieldDecorator('key', {\n          initialValue: initInfo.key,\n        })(<Input placeholder=\"please input upload key (if needed)\"></Input>)}\n      </Form.Item>\n    </Fragment>\n  );\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/piclist/index.ts",
    "content": "import Form from './form';\nimport { ImageHostingServiceMeta } from '../interface';\nimport Service from './service';\n\nexport default (): ImageHostingServiceMeta => {\n  return {\n    name: 'piclist',\n    icon: 'icons/piclist.png',\n    type: 'piclist',\n    service: Service,\n    form: Form,\n    permission: {\n      origins: ['<all_urls>'],    // often to be self-hosted\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/piclist/service.ts",
    "content": "import { IBasicRequestService } from '@/service/common/request';\nimport { RequestHelper } from '@/service/request/common/request';\nimport { UploadImageRequest, ImageHostingService } from '../interface';\nimport { Base64ImageToBlob } from '@/common/blob';\nimport Container from 'typedi';\nimport md5 from '@web-clipper/shared/lib/md5';\n\nexport interface PiclistImageHostingOption {\n  uploadUrl: string;\n  key: string;\n}\n\nexport default class PiclistImageHostingService implements ImageHostingService {\n  private config: PiclistImageHostingOption;\n\n  constructor(config: PiclistImageHostingOption) {\n    this.config = config;\n  }\n\n  getId = () => {\n    let uploadUrl = this.config.uploadUrl;\n    if (this.config.key) uploadUrl += `?key=${this.config.key}`;\n    return md5(uploadUrl); // as id\n  };\n\n  uploadImage = async ({ data }: UploadImageRequest) => {\n    const blob = Base64ImageToBlob(data);\n    return this.uploadBlob(blob, `web_cliper_image.png`);\n  };\n\n  uploadImageUrl = async (url: string) => {\n    const imageBlob = await Container.get(IBasicRequestService).download(url);\n    return this.uploadBlob(imageBlob, this._getImageFileName(url));\n  };\n\n  private uploadBlob = async (blob: Blob, fileName?: string): Promise<string> => {\n    const request = new RequestHelper({ request: Container.get(IBasicRequestService) });\n    let uploadUrl = this.config.uploadUrl;\n    if (this.config.key) uploadUrl += `?key=${this.config.key}`;\n    let formData = new FormData();\n    formData.append('image', blob, fileName);\n    let result = await request.postForm<{ success: boolean; result: string[] }>(uploadUrl, {\n      data: formData,\n    });\n    if (!result.success) throw new Error('Upload failed');\n    return result.result[0];\n  };\n  private _getImageFileName(url: string) {\n    // 分割路径和查询参数\n    const queryIndex = url.indexOf('?');\n    const pathPart = queryIndex === -1 ? url : url.slice(0, queryIndex);\n    const queryPart = queryIndex === -1 ? '' : url.slice(queryIndex + 1);\n\n    // 处理路径部分\n    const segments = pathPart.split('/');\n    let lastSegment = segments.pop() || '';\n\n    // 移除可能的哈希片段\n    const hashIndex = lastSegment.indexOf('#');\n    if (hashIndex !== -1) {\n      lastSegment = lastSegment.slice(0, hashIndex);\n    }\n\n    // 检查最后一段是否为文件名\n    if (lastSegment.includes('.')) {\n      return lastSegment;\n    }\n    let fileName = \"web_cliper_image\"\n    let fileExt: string = \"png\";\n    // 解析查询参数中的后缀\n    const queryParams = new URLSearchParams(queryPart);\n    const formatKeys = ['wx_fmt', 'format', 'fm', 'type'];\n    for (const key of formatKeys) {\n      if (queryParams.has(key)) {\n        fileExt = queryParams.get(key) as string;\n        if (fileExt) {\n          break;\n        }\n      }\n    }\n\n    // 检查路径中的其他段是否有已知图片后缀\n    const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'];\n    for (const seg of segments) {\n      const dotIndex = seg.lastIndexOf('.');\n      if (dotIndex !== -1) {\n        const ext = seg.slice(dotIndex + 1).toLowerCase();\n        if (imageExts.includes(ext)) {\n          fileExt = ext;\n          break;\n        }\n      }\n    }\n\n    // 默认返回空字符串\n    return `${fileName}.${fileExt}`;\n  }\n}\n"
  },
  {
    "path": "src/common/backend/imageHosting/qcloud/form.tsx",
    "content": "import React, { Fragment } from 'react';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input, InputNumber, Checkbox } from 'antd';\nimport { QcloudCosImageHostingOption } from './service';\n\ninterface Props extends FormComponentProps {\n  info: QcloudCosImageHostingOption;\n}\n\nexport default ({ form: { getFieldDecorator }, info }: Props) => {\n  const initInfo: Partial<Props['info']> = info || {};\n  return (\n    <Fragment>\n      <Form.Item label=\"Bucket\">\n        {getFieldDecorator('bucket', {\n          initialValue: initInfo.bucket,\n          rules: [\n            {\n              required: true,\n            },\n          ],\n        })(<Input placeholder=\"please input Bucket\"></Input>)}\n      </Form.Item>\n      <Form.Item label=\"Region\">\n        {getFieldDecorator('region', {\n          initialValue: initInfo.region,\n          rules: [\n            {\n              required: true,\n            },\n          ],\n        })(<Input placeholder=\"please input Region\"></Input>)}\n      </Form.Item>\n      <Form.Item label=\"Folder\">\n        {getFieldDecorator('folder', {\n          initialValue: initInfo.folder,\n          rules: [\n            {\n              required: true,\n            },\n          ],\n        })(<Input placeholder=\"please input Folder\"></Input>)}\n      </Form.Item>\n      <Form.Item label=\"SecretId\">\n        {getFieldDecorator('secretId', {\n          initialValue: initInfo.secretId,\n          rules: [\n            {\n              required: true,\n            },\n          ],\n        })(<Input placeholder=\"please input SecretId\"></Input>)}\n      </Form.Item>\n      <Form.Item label=\"SecretKey\">\n        {getFieldDecorator('secretKey', {\n          initialValue: initInfo.secretKey,\n          rules: [\n            {\n              required: true,\n            },\n          ],\n        })(<Input.Password placeholder=\"please input SecretKey\" min={0}></Input.Password>)}\n      </Form.Item>\n      <Form.Item label=\"PrivateRead\">\n        {getFieldDecorator('privateRead', {\n          initialValue: initInfo.privateRead,\n          valuePropName: 'checked',\n          rules: [\n            {\n              required: false,\n            },\n          ],\n        })(<Checkbox />)}\n      </Form.Item>\n      <Form.Item label=\"Expires\">\n        {getFieldDecorator('expires', {\n          initialValue: initInfo.expires,\n          rules: [\n            {\n              required: true,\n            },\n          ],\n        })(<InputNumber placeholder=\"please input Expires\"></InputNumber>)}\n      </Form.Item>\n    </Fragment>\n  );\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/qcloud/index.ts",
    "content": "import localeService from '@/common/locales';\nimport { ImageHostingServiceMeta } from '../interface';\nimport Service from './service';\nimport form from './form';\n\nexport default (): ImageHostingServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.qcloud.name',\n    }),\n    icon: 'qcloud',\n    type: 'qcloud',\n    form,\n    service: Service,\n    permission: {\n      origins: ['https://*.myqcloud.com/*'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/qcloud/service.ts",
    "content": "import { IBasicRequestService } from '@/service/common/request';\nimport { Base64ImageToBlob } from '@/common/blob';\nimport { UploadImageRequest, ImageHostingService } from '../interface';\nimport { generateUuid } from '@web-clipper/shared/lib/uuid';\nimport Container from 'typedi';\nimport { isUndefined } from 'lodash';\nimport COS from 'cos-js-sdk-v5';\n\nexport interface QcloudCosImageHostingOption {\n  bucket: string;\n  region: string;\n  folder: string;\n  secretId: string;\n  secretKey: string;\n  privateRead: boolean;\n  expires: number;\n}\n\nexport default class QcloudCosImageHostingService implements ImageHostingService {\n  private config: QcloudCosImageHostingOption;\n\n  constructor(config: QcloudCosImageHostingOption) {\n    this.config = config;\n  }\n\n  getId = () => {\n    return this.config.bucket;\n  };\n\n  uploadImage = async ({ data }: UploadImageRequest) => {\n    console.log('uploading...');\n    const blob = Base64ImageToBlob(data);\n    return this.uploadAsBlob(blob);\n  };\n\n  uploadImageUrl = async (url: string) => {\n    const imageBlob = await Container.get(IBasicRequestService).download(url);\n    return this.uploadAsBlob(imageBlob);\n  };\n\n  private generateFilename = (blob: Blob): string => {\n    const matchedSuffix: any = blob.type.match(/^image\\/(.*)/);\n    const suffix: string = matchedSuffix[1];\n    return `${generateUuid()}.${suffix}`;\n  };\n\n  private uploadAsBlob = async (blob: Blob): Promise<string> => {\n    if (isUndefined(this.config.folder)) this.config.folder = '';\n    if (this.config.folder.startsWith('/')) this.config.folder.substr(1);\n    if (!this.config.folder.endsWith('/') && this.config.folder.length > 0)\n      this.config.folder += '/';\n\n    const fileName = this.generateFilename(blob);\n    const date = new Date();\n    const folderName = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(\n      2,\n      '0'\n    )}${String(date.getDate()).padStart(2, '0')}`;\n    let cos = new COS({\n      SecretId: this.config.secretId,\n      SecretKey: this.config.secretKey,\n    });\n    await cos.putObject({\n      Bucket: this.config.bucket,\n      Region: this.config.region,\n      Key: `${this.config.folder}${folderName}/${fileName}`,\n      Body: blob,\n    });\n    return cos.getObjectUrl(\n      {\n        Bucket: this.config.bucket,\n        Region: this.config.region,\n        Key: `${this.config.folder}${folderName}/${fileName}`,\n        Sign: this.config.privateRead,\n        Expires: this.config.expires,\n      },\n      (err, data) => {\n        if (err) throw err;\n        return data.Url;\n      }\n    );\n  };\n}\n"
  },
  {
    "path": "src/common/backend/imageHosting/siyuan/index.ts",
    "content": "import localeService from '@/common/locales';\nimport { ImageHostingServiceMeta } from '../interface';\nimport Service from './service';\n\nexport default (): ImageHostingServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.imageHosting.siyuan.name',\n    }),\n    icon: 'siyuan',\n    type: 'siyuan',\n    service: Service,\n    builtIn: true,\n    builtInRemark: localeService.format({\n      id: 'backend.imageHosting.siyuan.builtInRemark',\n      defaultMessage: 'Siyuan Note built in image hosting service.',\n    }),\n  };\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/siyuan/service.ts",
    "content": "import { Base64ImageToBlob } from '@/common/blob';\nimport { Container } from 'typedi';\nimport { IBasicRequestService } from './../../../../service/common/request';\nimport { SiYuanClient } from './../../clients/siyuan/client';\nimport { UploadImageRequest, ImageHostingService } from '../interface';\nimport { Repository } from '../../services/interface';\nexport interface YuqueImageHostingOption {\n  access_token: string;\n}\n\nexport default class SiYuanImageHostingService implements ImageHostingService {\n  private context: { currentRepository: Repository } | null = null;\n  private siyuan: SiYuanClient;\n  constructor(config: { accessToken?: string }) {\n    this.siyuan = new SiYuanClient({\n      request: Container.get(IBasicRequestService),\n      accessToken: config.accessToken,\n    });\n  }\n\n  getId() {\n    return 'siyuan';\n  }\n\n  uploadImage = async ({ data }: UploadImageRequest) => {\n    const blob = Base64ImageToBlob(data);\n    return this.siyuan.uploadImage(blob);\n  };\n\n  uploadImageUrl = async (url: string) => {\n    let blob: Blob = await Container.get(IBasicRequestService).download(url);\n    if (blob.type === 'image/webp') {\n      blob = blob.slice(0, blob.size, 'image/jpeg');\n    }\n    return this.siyuan.uploadImage(blob);\n  };\n\n  updateContext = (context: { currentRepository: Repository }) => {\n    this.context = context;\n  };\n}\n"
  },
  {
    "path": "src/common/backend/imageHosting/sm.ms/form.tsx",
    "content": "import React from 'react';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\n\ninterface Props extends FormComponentProps {\n  info: {\n    secretToken: string;\n  };\n}\n\nexport default ({ form: { getFieldDecorator }, info }: Props) => {\n  const initInfo: Partial<Props['info']> = info || {};\n  return (\n    <Form.Item label=\"Secret Token\">\n      {getFieldDecorator('secretToken', {\n        initialValue: initInfo.secretToken,\n      })(<Input placeholder=\"please input Secret Token\"></Input>)}\n    </Form.Item>\n  );\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/sm.ms/index.ts",
    "content": "import { ImageHostingServiceMeta } from '../interface';\nimport Service from './service';\nimport form from './form';\n\nexport default (): ImageHostingServiceMeta => {\n  return {\n    name: 'sm.ms',\n    icon: 'smms',\n    type: 'sm.ms',\n    form,\n    service: Service,\n    permission: {\n      origins: ['https://sm.ms/*'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/sm.ms/service.ts",
    "content": "import { IBasicRequestService } from '@/service/common/request';\nimport { Base64ImageToBlob } from '@/common/blob';\nimport { UploadImageRequest, ImageHostingService } from '../interface';\nimport md5 from '@web-clipper/shared/lib/md5';\nimport Container from 'typedi';\nimport { RequestHelper } from '@/service/request/common/request';\n\nexport interface YuqueImageHostingOption {\n  host: string;\n}\n\nexport default class YuqueImageHostingService implements ImageHostingService {\n  private secretToken?: string;\n  constructor(info: { secretToken?: string }) {\n    this.secretToken = info.secretToken;\n  }\n  getId = () => {\n    return md5(this.secretToken ?? 'sm.ms');\n  };\n\n  uploadImage = async ({ data }: UploadImageRequest) => {\n    const blob = Base64ImageToBlob(data);\n    return this.uploadBlob(blob);\n  };\n\n  uploadImageUrl = async (url: string) => {\n    let blob: Blob = await Container.get(IBasicRequestService).download(url);\n    if (blob.type === 'image/webp') {\n      blob = blob.slice(0, blob.size, 'image/jpeg');\n    }\n    return this.uploadBlob(blob);\n  };\n\n  private uploadBlob = async (blob: Blob): Promise<string> => {\n    let formData = new FormData();\n    formData.append('smfile', blob);\n    formData.append('ssl', 'true');\n    let headers: { Authorization?: string } = {};\n    if (this.secretToken) {\n      headers.Authorization = this.secretToken;\n    }\n    const request = new RequestHelper({\n      request: Container.get(IBasicRequestService),\n      headers: headers,\n    });\n    const result = await request.postForm<\n      { data: { url: string } } | { code: string; success: false; images: string; message: string }\n    >(`https://sm.ms/api/v2/upload`, { data: formData });\n    if (isFail(result)) {\n      if (result.code !== 'image_repeated') {\n        throw new Error(result.message);\n      }\n      return result.images;\n    }\n    return result.data.url;\n  };\n}\n\nfunction isFail(\n  rs: { data: { url: string } } | { code: string; success: false; images: string }\n): rs is { code: string; success: false; images: string } {\n  if (!(rs as { code: string; success: false; images: string }).success) {\n    return true;\n  }\n  return false;\n}\n"
  },
  {
    "path": "src/common/backend/imageHosting/wiznote/index.ts",
    "content": "import localeService from '@/common/locales';\nimport { ImageHostingServiceMeta } from '../interface';\nimport Service from './service';\n\nexport default (): ImageHostingServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.imageHosting.wiznote.name',\n    }),\n    icon: 'wiznote',\n    type: 'WizNote',\n    service: Service,\n    builtIn: true,\n    builtInRemark: localeService.format({\n      id: 'backend.imageHosting.wiznote.builtInRemark',\n    }),\n  };\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/wiznote/service.ts",
    "content": "import { UploadImageRequest, ImageHostingService } from '../interface';\nimport { Base64ImageToBlob } from 'common/blob';\nimport Container from 'typedi';\nimport { IBasicRequestService } from '@/service/common/request';\nimport backend from 'common/backend';\nimport WizNoteDocumentService from 'common/backend/services/wiznote/service';\n\nexport interface WizImageHostingOption {\n  token: string;\n}\n\nexport default class WizNoteImageHostingService implements ImageHostingService {\n  getId() {\n    return 'wiznote';\n  }\n\n  uploadImage = async ({ data }: UploadImageRequest) => {\n    const blob = Base64ImageToBlob(data);\n    return this.uploadBlob(blob);\n  };\n\n  uploadImageUrl = async (url: string) => {\n    let blob: Blob = await Container.get(IBasicRequestService).download(url);\n    if (blob.type === 'image/webp') {\n      blob = blob.slice(0, blob.size, 'image/jpeg');\n    }\n    return this.uploadBlob(blob);\n  };\n\n  private uploadBlob = async (blob: Blob): Promise<string> => {\n    return (backend.getDocumentService()! as WizNoteDocumentService).uploadBlob(blob);\n  };\n}\n"
  },
  {
    "path": "src/common/backend/imageHosting/yuque_oauth/index.ts",
    "content": "import localeService from '@/common/locales';\nimport { ImageHostingServiceMeta } from '../interface';\nimport Service from './service';\n\nexport default (): ImageHostingServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.imageHosting.yuque_oauth.name',\n    }),\n    icon: 'yuque',\n    type: 'yuque_oauth',\n    service: Service,\n    builtIn: true,\n    builtInRemark: localeService.format({\n      id: 'backend.imageHosting.yuque_oauth.builtInRemark',\n    }),\n  };\n};\n"
  },
  {
    "path": "src/common/backend/imageHosting/yuque_oauth/service.ts",
    "content": "import { IBasicRequestService } from '@/service/common/request';\nimport { Base64ImageToBlob } from '@/common/blob';\nimport { UploadImageRequest, ImageHostingService } from '../interface';\nimport md5 from '@web-clipper/shared/lib/md5';\nimport { extend } from 'umi-request';\nimport localeService from '@/common/locales';\nimport Container from 'typedi';\n\nconst request = extend({});\n\nrequest.interceptors.response.use(\n  response => {\n    const codeMaps: {\n      [code: number]: string;\n    } = {\n      429: localeService.format({\n        id: 'backend.imageHosting.yuque_oauth.error_429',\n      }),\n      401: localeService.format({\n        id: 'backend.imageHosting.yuque_oauth.error_401',\n      }),\n      403: localeService.format({\n        id: 'backend.imageHosting.yuque_oauth.error_403',\n      }),\n    };\n    if (codeMaps[response.status]) {\n      throw new Error(codeMaps[response.status]);\n    }\n    return response;\n  },\n  { global: false }\n);\n\nexport interface YuqueImageHostingOption {\n  access_token: string;\n}\n\nconst HOST = 'https://www.yuque.com';\nconst BASE_URL = `${HOST}/api/v2/`;\n\nexport default class YuqueImageHostingService implements ImageHostingService {\n  private accessToken: string;\n\n  constructor({ access_token }: YuqueImageHostingOption) {\n    this.accessToken = access_token;\n  }\n\n  getId() {\n    return md5(this.accessToken);\n  }\n\n  uploadImage = async ({ data }: UploadImageRequest) => {\n    const blob = Base64ImageToBlob(data);\n    return this.uploadBlob(blob);\n  };\n\n  uploadImageUrl = async (url: string) => {\n    let blob: Blob = await Container.get(IBasicRequestService).download(url);\n    if (blob.type === 'image/webp') {\n      blob = blob.slice(0, blob.size, 'image/jpeg');\n    }\n    return this.uploadBlob(blob);\n  };\n\n  private uploadBlob = async (blob: Blob): Promise<string> => {\n    let formData = new FormData();\n    formData.append('file', blob, 'file.png');\n    const result = await request.post(`${BASE_URL}upload/attach`, {\n      data: formData,\n      requestType: 'form',\n      headers: {\n        'X-Auth-Token': this.accessToken,\n      },\n    });\n    return result.data.url;\n  };\n}\n"
  },
  {
    "path": "src/common/backend/index.ts",
    "content": "import {\n  ServiceMeta,\n  ImageHostingServiceMeta,\n  ImageHostingService,\n  DocumentService,\n} from './interface';\nexport * from './interface';\n\nconst serviceContext = require.context('./services', true, /index.ts$/);\n\nconst getServices = (): ServiceMeta[] => {\n  return serviceContext.keys().map(key => {\n    return serviceContext(key).default() as ServiceMeta;\n  });\n};\nconst imageHostingContext = require.context('./imageHosting', true, /index.ts$/);\n\nconst getImageHostingServices = (): ImageHostingServiceMeta[] => {\n  return imageHostingContext.keys().map(key => {\n    return imageHostingContext(key).default() as ImageHostingServiceMeta;\n  });\n};\n\nexport function documentServiceFactory(type: string, info?: any) {\n  const service = getServices().find(o => o.type === type);\n  if (service) {\n    const Service = service.service;\n    return new Service(info);\n  }\n  throw new Error('unSupport type');\n}\n\nexport function imageHostingServiceFactory(type: string, info?: any) {\n  const service = getImageHostingServices().find(o => o.type === type);\n  if (service) {\n    const Service = service.service;\n    return new Service(info);\n  }\n  throw new Error('un support image hosting type');\n}\n\nexport { getServices, getImageHostingServices };\n\nexport class BackendContext {\n  private documentService?: DocumentService;\n  private imageHostingService?: ImageHostingService;\n\n  setDocumentService(documentService: DocumentService) {\n    this.documentService = documentService;\n  }\n\n  getDocumentService() {\n    return this.documentService;\n  }\n\n  setImageHostingService(imageHostingService: ImageHostingService) {\n    this.imageHostingService = imageHostingService;\n  }\n\n  getImageHostingService() {\n    return this.imageHostingService;\n  }\n}\n\nexport default new BackendContext();\n"
  },
  {
    "path": "src/common/backend/interface.ts",
    "content": "export * from './imageHosting/interface';\nexport * from './services/interface';\n"
  },
  {
    "path": "src/common/backend/services/baklib/complete.tsx",
    "content": "import React from 'react';\n\nexport default ({ status: { edit_url } }: any) => {\n  return (\n    <div style={{ marginTop: 8 }}>\n      <a className=\"ant-btn-link\" type=\"link\" href={edit_url} target=\"_blank\">\n        编辑\n      </a>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/common/backend/services/baklib/form.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/es/form';\nimport React, { Fragment } from 'react';\nimport { BaklibBackendServiceConfig } from './interface';\nimport useOriginForm from '@/hooks/useOriginForm';\nimport { FormattedMessage } from 'react-intl';\n\ninterface BaklibFormProps {\n  verified?: boolean;\n  info?: BaklibBackendServiceConfig;\n}\n\nconst FormItem: React.FC<BaklibFormProps & FormComponentProps> = props => {\n  const {\n    form,\n    form: { getFieldDecorator },\n    info,\n    verified,\n  } = props;\n\n  const { verified: formVerified, handleAuthentication, formRules } = useOriginForm({\n    form,\n    initStatus: !!info,\n  });\n\n  let initData: Partial<BaklibBackendServiceConfig> = {};\n  if (info) {\n    initData = info;\n  }\n  let editMode = info ? true : false;\n  return (\n    <Fragment>\n      <Form.Item label=\"Host\">\n        {getFieldDecorator('origin', {\n          initialValue: initData.origin || 'https://www.baklib.com',\n          rules: [\n            {\n              required: true,\n              message: 'Host is required!',\n            },\n            ...formRules,\n          ],\n        })(\n          <Input.Search\n            enterButton={\n              <FormattedMessage\n                id=\"backend.services.baklib.form.authentication\"\n                defaultMessage=\"Authentication\"\n              />\n            }\n            disabled={editMode || formVerified}\n            onSearch={handleAuthentication}\n          />\n        )}\n      </Form.Item>\n      <Form.Item label=\"AccessToken\">\n        {getFieldDecorator('accessToken', {\n          initialValue: initData.accessToken,\n          rules: [\n            {\n              required: true,\n              message: 'AccessToken is required!',\n            },\n          ],\n        })(<Input disabled={editMode || verified || !formVerified} />)}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default FormItem;\n"
  },
  {
    "path": "src/common/backend/services/baklib/headerForm.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { TreeSelect } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment, useEffect } from 'react';\nimport locale from '@/common/locales';\nimport { Repository } from '../interface';\nimport { useFetch } from '@shihengtech/hooks';\nimport backend from '../..';\nimport BaklibDocumentService from './service';\n\nconst HeaderForm: React.FC<FormComponentProps & { currentRepository: Repository }> = ({\n  form: { getFieldDecorator, setFieldsValue, getFieldValue },\n  currentRepository,\n}) => {\n  const service = backend.getDocumentService() as BaklibDocumentService;\n  const channals = useFetch(() => {\n    if (currentRepository) {\n      return service.getTentChannel(currentRepository.id);\n    }\n    return [];\n  }, [currentRepository]);\n\n  useEffect(() => {\n    setFieldsValue({\n      channel: null,\n    });\n  }, [currentRepository, setFieldsValue]);\n\n  useEffect(() => {\n    if (Array.isArray(channals.data) && channals.data.length > 0 && !getFieldValue('channel')) {\n      setFieldsValue({\n        channel: channals.data[0].value,\n      });\n    }\n  }, [channals.data, getFieldValue, setFieldsValue]);\n  return (\n    <Fragment>\n      <Form.Item>\n        {getFieldDecorator('channel', {\n          rules: [],\n        })(\n          <TreeSelect\n            disabled={channals.loading}\n            loading={channals.loading}\n            allowClear\n            treeData={channals.data}\n            style={{ width: '100%' }}\n            placeholder={locale.format({\n              id: 'backend.services.baklib.headerForm.channel',\n              defaultMessage: 'Channel',\n            })}\n          />\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default HeaderForm;\n"
  },
  {
    "path": "src/common/backend/services/baklib/index.ts",
    "content": "import { ServiceMeta } from './../interface';\nimport Service from './service';\nimport Form from './form';\nimport localeService from '@/common/locales';\nimport headerForm from './headerForm';\nimport complete from './complete';\n\nexport default (): ServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.baklib.name',\n    }),\n    complete,\n    icon: 'baklib',\n    type: 'baklib',\n    service: Service,\n    form: Form,\n    headerForm,\n    homePage: 'https://www.baklib.com/',\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/baklib/interface.ts",
    "content": "export interface BaklibBackendServiceConfig {\n  accessToken: string;\n  origin: string;\n}\n\nexport interface BaklibTenantsResponse {\n  current_tenants: { id: string; name: string; member_role: string[] }[];\n  share_tenants: { id: string; name: string; member_role: string[] }[];\n}\n"
  },
  {
    "path": "src/common/backend/services/baklib/service.ts",
    "content": "import { DocumentService, CreateDocumentRequest } from './../../index';\nimport { extend, RequestMethod } from 'umi-request';\nimport md5 from '@web-clipper/shared/lib/md5';\nimport { BaklibBackendServiceConfig, BaklibTenantsResponse } from './interface';\nimport { CompleteStatus, Repository } from '../interface';\n\ninterface Channel {\n  id: string;\n  name: string;\n  ordinal: number;\n  child_channels: Channel[];\n}\n\nexport default class BaklibDocumentService implements DocumentService {\n  private request: RequestMethod;\n  private token: string;\n  private cache: Map<string, any>;\n  private origin: string;\n\n  constructor({ accessToken, origin }: BaklibBackendServiceConfig) {\n    const realHost = origin || 'https://www.baklib.com';\n    this.request = extend({\n      prefix: `${realHost}/api/`,\n      headers: { Authorization: `Bearer ${accessToken}` },\n      timeout: 5000,\n    });\n    this.request.interceptors.response.use(\n      async response => {\n        const json = await response.clone().json();\n        if (json.code !== 0) {\n          throw new Error(json.message);\n        }\n        return response;\n      },\n      { global: false }\n    );\n    this.token = accessToken;\n    this.cache = new Map<string, any>();\n    this.origin = origin;\n  }\n\n  getId = () => md5(this.token);\n\n  getUserInfo = async () => {\n    return {\n      name: 'Baklib',\n      avatar: '',\n      homePage: `${this.origin}/-/groups`,\n      description: 'Baklib',\n    };\n  };\n\n  getRepositories = async () => {\n    const {\n      message: { current_tenants, share_tenants },\n    } = await this.request.get<{\n      message: BaklibTenantsResponse;\n    }>('v1/tenants');\n    function tenantToRepo(tenants: any, groupName: string) {\n      return tenants.map(\n        ({ id, name, member_role }: any): Repository => {\n          const readOnly = Array.isArray(member_role) && member_role[0] === '只能阅读';\n          return {\n            id,\n            name: readOnly ? `${name} (只读)` : name,\n            disabled: readOnly,\n            groupId: groupName,\n            groupName,\n          };\n        }\n      );\n    }\n    return tenantToRepo(current_tenants, '我的站点').concat(\n      tenantToRepo(share_tenants, '共享站点')\n    );\n  };\n\n  async getTentChannel(tenant_id: string) {\n    if (this.cache.has(tenant_id)) {\n      return this.cache.get(tenant_id);\n    }\n    const response = await this.request.get<{\n      message: Channel[];\n    }>(`v1/channels?tenant_id=${tenant_id}`);\n    const { message } = response;\n    function channelToTree(tree: Channel[], parent: string): any {\n      tree.sort((a, b) => a.ordinal - b.ordinal);\n      return tree.map((o, index) => ({\n        title: o.name,\n        value: o.id,\n        key: `${parent}-${index}`,\n        children: channelToTree(o.child_channels, `${parent}-${index}`),\n      }));\n    }\n    this.cache.set(tenant_id, channelToTree(message, '0'));\n    return channelToTree(message, '0');\n  }\n\n  createDocument = async (\n    info: CreateDocumentRequest & {\n      channel: string;\n      status: number;\n    }\n  ): Promise<CompleteStatus & { edit_url: string }> => {\n    const response = await this.request.post<{\n      message: {\n        id: string;\n        frontend_url: string;\n        edit_url: string;\n      };\n    }>('v1/articles', {\n      data: {\n        content_type: 'markdown',\n        tenant_id: info.repositoryId,\n        name: info.title,\n        channel_id: info.channel,\n        content: info.content,\n        status: 1,\n      },\n    });\n    return {\n      href: response.message.frontend_url,\n      edit_url: response.message.edit_url,\n    };\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/bear/form.tsx",
    "content": "import React from 'react';\nimport { FormattedMessage } from 'react-intl';\n\nexport default () => (\n  <div style={{ textAlign: 'right' }}>\n    <FormattedMessage\n      id=\"backend.services.bear.form.confirm\"\n      defaultMessage=\"Please confirm that the Bear client is installed.\"\n    />\n  </div>\n);\n"
  },
  {
    "path": "src/common/backend/services/bear/index.ts",
    "content": "import Service from './service';\nimport Form from './form';\n\nexport default () => {\n  return {\n    name: 'Bear',\n    icon: 'bear',\n    type: 'bear',\n    service: Service,\n    form: Form,\n    homePage: 'https://bear.app/',\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/bear/service.ts",
    "content": "import { CompleteStatus } from 'common/backend/interface';\nimport { DocumentService, CreateDocumentRequest } from '../../index';\n\nexport default class GithubDocumentService implements DocumentService {\n  getId = () => {\n    return 'bear';\n  };\n\n  getUserInfo = async () => {\n    return {\n      name: 'BEAR',\n      avatar: '',\n      homePage: 'bear://x-callback-url/search',\n      description: 'Bear app',\n    };\n  };\n\n  getRepositories = async () => {\n    return [\n      {\n        id: 'bear',\n        name: 'Bear',\n        groupId: 'bear',\n        groupName: 'Bear',\n      },\n    ];\n  };\n\n  createDocument = async (info: CreateDocumentRequest): Promise<CompleteStatus> => {\n    const url = `bear://x-callback-url/create?title=${encodeURIComponent(\n      info.title\n    )}&text=${encodeURIComponent(info.content)}&open_note=no`;\n    window.location.href = url;\n    return {\n      href: `bear://x-callback-url/open-note?title=${info.title}`,\n    };\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/buildin/index.ts",
    "content": "import { ServiceMeta } from '@/common/backend';\nimport Service from './service';\n\nexport const buildinOrigin = 'https://buildin.ai';\n\nexport default (): ServiceMeta => {\n  return {\n    name: 'Buildin.AI',\n    icon: 'https://cdn.buildin.ai/s3-public/8ebf3bb6-08c9-40b1-93d5-6d5c5c2fe49c/logo.svg',\n    type: 'buildin',\n    homePage: 'https://buildin.ai/',\n    service: Service,\n    permission: {\n      origins: [`${buildinOrigin}/*`, '<all_urls>'],\n      permissions: ['cookies'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/buildin/service.ts",
    "content": "import { CompleteStatus, UnauthorizedError } from '../interface';\nimport { DocumentService, CreateDocumentRequest } from '../../index';\nimport localeService from '@/common/locales';\nimport { extend, RequestMethod } from 'umi-request';\nimport { IWebRequestService, WebBlockHeader } from '@/service/common/webRequest';\nimport Container from 'typedi';\nimport { ICookieService } from '@/service/common/cookie';\nimport {\n  BuildinToc,\n  BuildinRepository,\n  BuildinSpace,\n  BuildinUserInfo,\n  Block,\n  OSSInfo,\n  TaskResult,\n  BuildinResponse,\n  ROLE_WEIGHT,\n  Share,\n} from './type';\nimport { generateUuid } from '@web-clipper/shared/lib/uuid';\nimport { buildinOrigin } from '.';\nimport showdown from 'showdown';\nconst converter = new showdown.Converter({});\nexport default class BuildinDocumentService implements DocumentService {\n  private request: RequestMethod;\n  private repositories: BuildinRepository[];\n  private userSpaces?: BuildinSpace;\n  private tocPageBlocks?: Record<string, Block>;\n  private userInfo?: BuildinUserInfo;\n  private webRequestService: IWebRequestService;\n  private cookieService: ICookieService;\n\n  constructor() {\n    const request = extend({\n      prefix: `${buildinOrigin}/api/`,\n      timeout: 10000,\n      credentials: 'include',\n    });\n    this.request = request;\n    this.repositories = [];\n    this.webRequestService = Container.get(IWebRequestService);\n    this.cookieService = Container.get(ICookieService);\n\n    request.interceptors.response.use(\n      response => {\n        if (response.status === 401) {\n          throw new UnauthorizedError(\n            localeService.format({\n              id: 'backend.services.buildin.unauthorizedErrorMessage',\n              defaultMessage: 'Unauthorized! Please Login Buildin.ai Web.',\n            })\n          );\n        }\n        return response;\n      },\n      { global: false }\n    );\n  }\n\n  getId = () => {\n    return 'Buildin.AI';\n  };\n\n  getUserInfo = async () => {\n    if (!this.userInfo) {\n      const res = await this.fetchUserInfo();\n      this.userInfo = res.data;\n    }\n    const { nickname, avatar, ext } = this.userInfo;\n    return {\n      name: nickname,\n      avatar: avatar?.startsWith('http') ? avatar : getImageCdnUrl(avatar),\n      homePage: 'https://buildin.ai',\n      description: ext?.email?.email,\n    };\n  };\n\n  getRepositories = async () => {\n    if (!this.userInfo) {\n      const res = await this.fetchUserInfo();\n      this.userInfo = res.data;\n    }\n    if (!this.userSpaces) {\n      const res = await this.getUserSpaces();\n      this.userSpaces = res.data;\n    }\n\n    const { spaceViews, spaces } = this.userSpaces;\n\n    if (!spaceViews || !spaces) {\n      this.repositories = [];\n      return [];\n    }\n\n    const result: BuildinRepository[] = [];\n    //fetch spaces\n    const userSpaces = Object.values(spaceViews)\n      .filter(spaceView => spaces[spaceView.spaceId])\n      .map(spaceView => spaces[spaceView.spaceId]);\n\n    if (!this.tocPageBlocks) {\n      const allPromise = userSpaces.map(space => {\n        return this.getSpaceRoot(space.uuid);\n      });\n      const allToc = await Promise.all(allPromise);\n      this.tocPageBlocks = allToc.reduce((pre, cur) => {\n        if (!cur.data.blocks) return pre;\n        Object.values(cur.data.blocks).forEach(b => {\n          //the pages which can be saved\n          if ([0, 18, 19].includes(b.type)) {\n            pre[b.uuid] = b;\n          }\n        });\n        return pre;\n      }, {} as Record<string, Block>);\n\n      userSpaces.forEach(sp => {\n        sp.subNodes.forEach(id => {\n          const block = this.tocPageBlocks?.[id];\n          if (!block) return;\n          if (block.permissions.some(o => o.type === 'illegal')) return;\n          if (block.permissions.length === 0) return;\n          const { role } = getPermission(block, this.userInfo?.uuid!, sp.permissionGroups ?? []);\n          if (role === 'editor' || role === 'writer') {\n            const spaceId = block.spaceId ?? sp.uuid;\n            let groupName = sp.title;\n            result.push({\n              id: block.uuid,\n              name: block.title || 'Untitled',\n              groupId: spaceId,\n              groupName,\n            });\n          }\n        });\n      });\n    }\n    this.repositories = result;\n    return result;\n  };\n\n  createDocument = async ({\n    repositoryId,\n    title,\n    content,\n  }: CreateDocumentRequest): Promise<CompleteStatus> => {\n    const repository = this.repositories.find(o => o.id === repositoryId);\n    if (!repository) {\n      throw new Error('Illegal repository');\n    }\n    const documentId = await this.createEmptyPage(repository, title);\n    const html = `<!DOCTYPE html>\n    <html>\n      <head>\n        <title>${title}</title>\n      </head>\n      <body>\n       ${converter.makeHtml(`${content}`)}\n      </body>\n    </html>`;\n    const ossInfo = await this.requestWithCookie<BuildinResponse<OSSInfo>>(header => {\n      return this.request.post(`import_temp_file?source=web-clipper`, {\n        headers: {\n          [header.name]: header.value,\n        },\n        data: {\n          content: html,\n          extName: 'html',\n        },\n      });\n    });\n    if (ossInfo.code !== 200) {\n      throw new Error('upload md content failed');\n    }\n    //call import api\n    const res = await this.requestWithCookie<BuildinResponse<{ taskId: string }>>(header => {\n      return this.request.post('enqueueTask', {\n        headers: {\n          [header.name]: header.value,\n        },\n        data: {\n          eventName: 'import',\n          request: {\n            blockId: documentId,\n            spaceId: repository.groupId,\n            importOptions: {\n              type: 'html',\n              ossName: ossInfo.data.ossName,\n            },\n          },\n        },\n      });\n    });\n    if (!res.data.taskId) {\n      throw new Error('enqueueTask failed');\n    }\n    const taskId = res.data.taskId;\n\n    const waitResult = async () => {\n      await sleep(2000);\n      const res = await this.requestWithCookie<BuildinResponse<TaskResult>>(header => {\n        return this.request.post('getTasks', {\n          headers: {\n            [header.name]: header.value,\n          },\n          data: {\n            taskIds: [taskId],\n          },\n        });\n      });\n      if (res.code !== 200) {\n        throw new Error('getTasks failed');\n      }\n      const result = res.data.results[taskId];\n      if (result && result.status === 'success') {\n        if (result.result?.status === 'success') {\n          // do nothing\n        } else if (result.result?.msg) {\n          throw new Error(result.result?.msg);\n        }\n      } else {\n        await waitResult();\n      }\n    };\n    await waitResult();\n    this.changeTitle(documentId, repository.groupId, title);\n    return {\n      href: `${buildinOrigin}/${documentId}`,\n    };\n  };\n\n  createEmptyPage = async (repository: BuildinRepository, title: string) => {\n    if (!this.tocPageBlocks) {\n      throw new Error('Illegal tocBlocks');\n    }\n    const documentId = generateUuid();\n    const parentId = repository.id;\n    const spaceId = repository.groupId;\n    const blocks = this.tocPageBlocks;\n    if (!blocks) {\n      throw new Error('Illegal blocks');\n    }\n    const subNodes = blocks[parentId].subNodes;\n    const after = subNodes[subNodes.length - 1];\n\n    const operations = {\n      requestId: generateUuid(),\n      transactions: [\n        {\n          id: generateUuid(),\n          spaceId,\n          operations: [\n            {\n              id: documentId,\n              path: [],\n              command: 'set',\n              table: 'block',\n              args: {\n                uuid: documentId,\n                spaceId,\n                parentId,\n                textColor: '',\n                backgroundColor: '',\n                type: 0,\n                status: 1,\n                permissions: [],\n                updateBy: this.userInfo?.uuid,\n                updateAt: Date.now(),\n                data: {\n                  segments: [{ type: 0, text: title, enhancer: {} }],\n                },\n              },\n            },\n            {\n              id: parentId,\n              command: 'listAfter',\n              path: ['subNodes'],\n              table: 'block',\n              args: {\n                uuid: documentId,\n                after,\n              },\n            },\n          ],\n        },\n      ],\n    };\n    await this.requestWithCookie(header => {\n      return this.request.post('blocks/transactions', {\n        data: operations,\n        headers: {\n          [header.name]: header.value,\n        },\n      });\n    });\n    return documentId;\n  };\n  private changeTitle = async (documentId: string, spaceId: string, title: string) => {\n    const operations = {\n      requestId: generateUuid(),\n      transactions: [\n        {\n          id: generateUuid(),\n          spaceId,\n          operations: [\n            {\n              id: documentId,\n              path: ['data'],\n              command: 'update',\n              table: 'block',\n              args: {\n                segments: [{ type: 0, text: title, enhancer: {} }],\n              },\n            },\n          ],\n        },\n      ],\n    };\n    await this.requestWithCookie(header => {\n      return this.request.post('blocks/transactions', {\n        data: operations,\n        headers: {\n          [header.name]: header.value,\n        },\n      });\n    });\n  };\n\n  private getUserSpaces = async () => {\n    return this.requestWithCookie<BuildinResponse<BuildinSpace>>(header => {\n      return this.request.get(`users/${this.userInfo?.uuid}/root`, {\n        headers: {\n          [header.name]: header.value,\n        },\n      });\n    });\n  };\n  private getSpaceRoot = async (spaceId: string) => {\n    return this.requestWithCookie<BuildinToc>(header => {\n      return this.request.get<BuildinToc>(`spaces/${spaceId}/root`, {\n        headers: {\n          [header.name]: header.value,\n        },\n      });\n    });\n  };\n\n  private fetchUserInfo = async () => {\n    return this.requestWithCookie<BuildinResponse<BuildinUserInfo>>(header => {\n      return this.request.get('users/me', {\n        headers: {\n          [header.name]: header.value,\n        },\n      });\n    });\n  };\n\n  /**\n   * Modify the cookie when request\n   */\n  private requestWithCookie = async <T>(\n    requestFunction: (header: WebBlockHeader) => Promise<T>\n  ) => {\n    const cookies = await this.cookieService.getAll({\n      url: buildinOrigin,\n    });\n    const cookieString = cookies.map(o => `${o.name}=${o.value}`).join(';');\n    const header = await this.webRequestService.startChangeHeader({\n      urls: [`${buildinOrigin}*`],\n      requestHeaders: [\n        {\n          name: 'cookie',\n          value: cookieString,\n        },\n      ],\n    });\n    try {\n      const result = await requestFunction(header);\n      await this.webRequestService.end(header);\n      return result;\n    } catch (error) {\n      await this.webRequestService.end(header);\n      throw error;\n    }\n  };\n}\n\nconst compressImageSupport = /^(jpg|jpeg|png|bmp|webp|tiff)$/i;\nfunction getImageCdnUrl(ossName?: string) {\n  if (!ossName) return '';\n  const index = ossName.lastIndexOf('.');\n  const extName = ossName.substring(index + 1);\n  let imgProcess = '';\n  if (compressImageSupport.test(extName.toLocaleLowerCase())) {\n    imgProcess = `img_process=/resize,w_${500 * Math.ceil(window.devicePixelRatio)}/quality,q_80/`;\n  }\n  return `https://cdn.buildin.ai/${ossName}?${imgProcess}`;\n}\nconst sleep = (durationInMs: number): Promise<void> => {\n  return new Promise(resolve => {\n    setTimeout(resolve, durationInMs);\n  });\n};\n\nconst getPermission = (block: Block, userId: string, permissionGroups: any[]) => {\n  const data: Share = {\n    shared: false,\n    illegal: false,\n    isRestricted: false,\n    allowDuplicate: true,\n    permissions: [],\n    role: 'none',\n    roleWithoutPublic: 'none',\n  };\n  if (block.permissions.length) {\n    const getBiggerRole = (\n      type: keyof Block['permissions'][0],\n      value: any,\n      role?: keyof typeof ROLE_WEIGHT\n    ) => {\n      const permissions = block.permissions.find(p => p[type] === value);\n      if (\n        permissions &&\n        role &&\n        permissions.role &&\n        ROLE_WEIGHT[permissions.role] > ROLE_WEIGHT[role]\n      ) {\n        return permissions;\n      }\n    };\n    const newPermissions = block.permissions\n      .filter(o => {\n        return o.type !== 'illegal' && o.type !== 'restricted';\n      })\n      .map(o => {\n        if (o.type === 'space') {\n          return getBiggerRole('type', o.type, o.role) || o;\n        }\n        if (o.type === 'group') {\n          return getBiggerRole('groupId', o.groupId, o.role) || o;\n        }\n        if (o.type === 'user') {\n          return getBiggerRole('userId', o.userId, o.role) || o;\n        }\n        return o;\n      });\n    const diffPermissions = block.permissions.filter(o => {\n      if (o.type === 'illegal' || o.type === 'restricted') {\n        return false;\n      }\n      if (o.type === 'space' || o.type === 'public') {\n        return newPermissions.every(p => p.type !== o.type);\n      }\n      if (o.type === 'group') {\n        return newPermissions.every(p => p.groupId !== o.groupId);\n      }\n      return newPermissions.every(p => p.userId !== o.userId);\n    });\n    data.permissions = [...newPermissions, ...diffPermissions];\n    const ownPermission = data.permissions.find(p => p.userId === userId);\n    const groupPermissions = data.permissions.filter(p => {\n      const group = permissionGroups?.find(g => g.id === p.groupId);\n      return group?.userIds.includes(userId);\n    });\n    const allPermissions = [ownPermission, ...groupPermissions];\n    const spacePermission = block.permissions.find(p => p.type === 'space');\n    allPermissions.push(spacePermission);\n    data.roleWithoutPublic = allPermissions.reduce(\n      (pre: keyof typeof ROLE_WEIGHT, permission: Block['permissions'][0] | undefined) => {\n        if (!permission) return pre;\n        const role = permission.role ?? 'none';\n        return ROLE_WEIGHT[role] > ROLE_WEIGHT[pre] ? role : pre;\n      },\n      'none'\n    );\n    data.role = data.roleWithoutPublic;\n  }\n\n  return data;\n};\n"
  },
  {
    "path": "src/common/backend/services/buildin/type.ts",
    "content": "import { Repository } from '../interface';\n\ninterface Space {\n  uuid: string;\n  title: string;\n  subNodes: string[];\n  permissionGroups?: any[];\n}\ninterface SpaceView {\n  uuid: string;\n  spaceId: string;\n  title: string;\n}\ntype BlockType = number;\nexport interface Block {\n  uuid: string;\n  spaceId: string;\n  parentId: string;\n  type: BlockType;\n  title: string;\n  subNodes: string[];\n  permissions: {\n    type: string;\n    role?: keyof typeof ROLE_WEIGHT;\n    userId?: string;\n    groupId?: string;\n  }[];\n}\nexport interface BuildinSpace {\n  spaces: Record<string, Space>;\n  spaceViews: Record<string, SpaceView>;\n}\nexport interface OSSInfo {\n  ossName: string;\n}\nexport interface BuildinToc {\n  data: {\n    blocks?: Record<string, Block>;\n  };\n}\n\nexport interface BuildinUserInfo {\n  uuid: string;\n  phone: string;\n  nickname: string;\n  backgroundColor: string;\n  spaceViews: string;\n  avatar?: string;\n  ext?: {\n    email?: {\n      id: string;\n      email: string;\n    };\n  };\n}\n\nexport interface BuildinRepository extends Repository {}\n\nexport interface TaskResult {\n  results: Record<\n    string,\n    {\n      taskId: string;\n      eventName: string;\n      status: string;\n      result?: {\n        status?: string;\n        url?: string;\n        size?: number;\n        ossName?: string;\n        uuid?: string;\n        msg?: string;\n      };\n    }\n  >;\n}\n\nexport interface BuildinResponse<DATA> {\n  msg: string;\n  code: number;\n  data: DATA;\n}\n\nexport const ROLE_WEIGHT = {\n  none: 0,\n  reader: 1,\n  writer: 2,\n  editor: 3,\n  commenter: 4,\n};\n\nexport interface Share {\n  shared: boolean;\n  title?: string;\n  illegal: boolean;\n  parentId?: string;\n  isRestricted: boolean;\n  /** 允许复制、打印、下载 */\n  allowDuplicate: boolean;\n  permissions: Block['permissions'];\n  role: keyof typeof ROLE_WEIGHT;\n  roleWithoutPublic: keyof typeof ROLE_WEIGHT;\n}\n"
  },
  {
    "path": "src/common/backend/services/confluence/form.tsx",
    "content": "import React from 'react';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input, Select } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport { useFetch } from '@shihengtech/hooks';\nimport { extend } from 'umi-request';\nimport {\n  ConfluenceListResult,\n  ConfluenceSpace,\n  ConfluenceServiceConfig,\n} from '@/common/backend/services/confluence/interface';\nimport { FormattedMessage } from 'react-intl';\nimport useOriginForm from '@/hooks/useOriginForm';\n\ninterface ConfluenceFormProps extends FormComponentProps {\n  info?: ConfluenceServiceConfig;\n}\n\nconst ConfluenceForm: React.FC<ConfluenceFormProps> = ({ form, info }) => {\n  const { verified, handleAuthentication, formRules } = useOriginForm({ form, initStatus: !!info });\n\n  const host = form.getFieldValue('origin');\n\n  const spaces = useFetch(\n    async () => {\n      if (!verified) {\n        return [];\n      }\n      const request = extend({\n        prefix: host,\n      });\n      const spaceList = await request.get<ConfluenceListResult<ConfluenceSpace>>(`/rest/api/space`);\n      return spaceList.results;\n    },\n    [host, verified],\n    {\n      initialState: {\n        data: [],\n      },\n    }\n  );\n\n  return (\n    <React.Fragment>\n      <Form.Item\n        label={\n          <FormattedMessage id=\"backend.services.confluence.form.origin\" defaultMessage=\"Origin\" />\n        }\n      >\n        {form.getFieldDecorator('origin', {\n          initialValue: info?.origin,\n          rules: formRules,\n        })(\n          <Input.Search\n            enterButton={\n              <FormattedMessage\n                id=\"backend.services.confluence.form.authentication\"\n                defaultMessage=\"Authentication\"\n              />\n            }\n            onSearch={handleAuthentication}\n            disabled={verified}\n          />\n        )}\n      </Form.Item>\n      {verified && (\n        <Form.Item\n          label={\n            <FormattedMessage id=\"backend.services.confluence.form.space\" defaultMessage=\"Space\" />\n          }\n        >\n          {form.getFieldDecorator('spaceId', {\n            initialValue: info?.spaceId,\n            rules: [\n              {\n                required: true,\n              },\n            ],\n          })(\n            <Select loading={spaces.loading}>\n              {spaces\n                .data!.filter(o => !!o._expandable.homepage)\n                .map(o => {\n                  const spaceHomePage = o._expandable.homepage!.split('/');\n                  return (\n                    <Select.Option key={o.id} value={`${spaceHomePage[spaceHomePage.length - 1]}`}>\n                      {o.name}\n                    </Select.Option>\n                  );\n                })}\n            </Select>\n          )}\n        </Form.Item>\n      )}\n    </React.Fragment>\n  );\n};\n\nexport default ConfluenceForm;\n"
  },
  {
    "path": "src/common/backend/services/confluence/index.ts",
    "content": "import Service from './service';\nimport Form from './form';\n\nexport default () => {\n  return {\n    name: 'Confluence',\n    icon: 'confluence',\n    type: 'confluence',\n    service: Service,\n    form: Form,\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/confluence/interface.ts",
    "content": "export interface ConfluenceListResult<T> {\n  results: T[];\n  start: number;\n  limit: number;\n  size: number;\n}\n\nexport interface ConfluenceSpace {\n  id: number;\n  name: string;\n  type: string;\n  _expandable: {\n    homepage?: string;\n  };\n}\n\nexport interface ConfluencePage {\n  id: string;\n  title: string;\n  type: string;\n}\n\nexport interface ConfluenceServiceConfig {\n  origin: string;\n  spaceId: number;\n}\n\nexport interface ConfluenceSpace {\n  id: number;\n  name: string;\n  type: string;\n}\n\nexport interface ConfluenceUserInfo {\n  displayName: string;\n  profilePicture: {\n    path: string;\n  };\n}\n\n/**\n * Response of rest/api/content/:id\n */\nexport interface ConfluenceSpaceContent {\n  space: {\n    key: string;\n    id: number;\n    name: string;\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/confluence/service.ts",
    "content": "import md5 from '@web-clipper/shared/lib/md5';\nimport {\n  ConfluenceServiceConfig,\n  ConfluenceUserInfo,\n  ConfluenceListResult,\n  ConfluencePage,\n  ConfluenceSpaceContent,\n} from '@/common/backend/services/confluence/interface';\nimport { DocumentService } from '../../index';\nimport { extend, RequestMethod } from 'umi-request';\nimport { Repository, CreateDocumentRequest, CompleteStatus } from '../interface';\nimport showdown from 'showdown';\n\nconst converter = new showdown.Converter();\n\nexport default class GithubDocumentService implements DocumentService {\n  private config: ConfluenceServiceConfig;\n  private request: RequestMethod;\n\n  constructor(config: ConfluenceServiceConfig) {\n    this.config = config;\n    this.request = extend({\n      prefix: `${this.config.origin}/rest/api/`,\n    });\n  }\n\n  getId = () => {\n    return md5(`${this.config.origin}:${this.config.spaceId}`);\n  };\n\n  getUserInfo = async () => {\n    const response = await this.request.get<ConfluenceUserInfo>('user/current');\n    return {\n      name: response.displayName,\n      avatar: `${this.config.origin}${response.profilePicture.path}`,\n      homePage: '',\n      description: 'Confluence user',\n    };\n  };\n\n  getRepositories = async (): Promise<Repository[]> => {\n    const confluenceSpaceContent = await this.request.get<ConfluenceSpaceContent>(\n      `content/${this.config.spaceId}`\n    );\n    const response = await this.request.get<ConfluenceListResult<ConfluencePage>>(\n      `content/${this.config.spaceId}/child/page`\n    );\n    return response.results.map(({ id, title }) => ({\n      id: id,\n      name: title,\n      groupId: confluenceSpaceContent.space.key,\n      groupName: confluenceSpaceContent.space.name,\n    }));\n  };\n\n  createDocument = async (req: CreateDocumentRequest): Promise<CompleteStatus> => {\n    const confluenceSpaceContent = await this.request.get<ConfluenceSpaceContent>(\n      `content/${this.config.spaceId}`\n    );\n    const response = await this.request.post<{\n      _links: {\n        webui: string;\n      };\n    }>('content', {\n      data: {\n        type: 'page',\n        title: req.title,\n        ancestors: [{ id: req.repositoryId }],\n        space: { key: confluenceSpaceContent.space.key },\n        body: { storage: { value: converter.makeHtml(req.content), representation: 'storage' } },\n      },\n    });\n    return {\n      href: `${this.config.origin}${response._links.webui}`,\n    };\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/dida365/headerForm.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Select } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport backend from '../..';\nimport Dida365DocumentService from './service';\nimport locale from '@/common/locales';\nimport { useFetch } from '@shihengtech/hooks';\n\nconst HeaderForm: React.FC<FormComponentProps> = ({ form: { getFieldDecorator } }) => {\n  const service = backend.getDocumentService() as Dida365DocumentService;\n  const tagsResponse = useFetch(async () => service.getTags(), [service], {\n    initialState: {\n      data: [],\n    },\n  });\n\n  return (\n    <Fragment>\n      <Form.Item>\n        {getFieldDecorator('tags', {\n          initialValue: [],\n        })(\n          <Select\n            mode=\"tags\"\n            maxTagCount={3}\n            style={{ width: '100%' }}\n            placeholder={locale.format({\n              id: 'backend.services.dida365.headerForm.applyTags',\n              defaultMessage: 'Apply tags',\n            })}\n            loading={tagsResponse.loading}\n          >\n            {tagsResponse.data?.map(o => (\n              <Select.Option key={o} value={o} title={o}>\n                {o}\n              </Select.Option>\n            ))}\n          </Select>\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default HeaderForm;\n"
  },
  {
    "path": "src/common/backend/services/dida365/index.ts",
    "content": "import localeService from '@/common/locales';\nimport { ServiceMeta } from '@/common/backend';\nimport Service from './service';\nimport headerForm from './headerForm';\n\nexport default (): ServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.dida365.name',\n    }),\n    icon: 'dida365',\n    type: 'dida365',\n    headerForm,\n    service: Service,\n    permission: {\n      origins: ['https://api.dida365.com/*'],\n      permissions: [],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/dida365/service.ts",
    "content": "import { Container } from 'typedi';\nimport { generateUuid } from '@web-clipper/shared/lib/uuid';\nimport localeService from '@/common/locales';\nimport { IWebRequestService } from '@/service/common/webRequest';\nimport {\n  CreateDocumentRequest,\n  CompleteStatus,\n  UnauthorizedError,\n  Repository,\n} from '@/common/backend/services/interface';\nimport { DocumentService } from '@/common/backend/index';\nimport { extend, RequestMethod } from 'umi-request';\n\ninterface Dida365Profile {\n  name: string;\n  username: string;\n  picture: string;\n}\n\ninterface Dida365CheckResponse {\n  projectProfiles: {\n    id: string;\n    name: string;\n    isOwner: boolean;\n    closed: boolean;\n    groupId: string;\n  }[];\n  projectGroups?: {\n    id: string;\n    name: string;\n  }[];\n  tags?: {\n    name: string;\n  }[];\n}\ninterface Dida365CreateDocumentRequest extends CreateDocumentRequest {\n  tags: string[];\n}\n\nexport default class Dida365DocumentService implements DocumentService {\n  private request: RequestMethod;\n\n  constructor() {\n    const request = extend({\n      prefix: `https://api.dida365.com/api/v2/`,\n    });\n    request.interceptors.response.use(\n      (response) => {\n        if (response.clone().status === 401) {\n          throw new UnauthorizedError(\n            localeService.format({\n              id: 'backend.services.dida365.unauthorizedErrorMessage',\n              defaultMessage: 'Unauthorized! Please Login Dida365 Web.',\n            })\n          );\n        }\n        return response;\n      },\n      { global: false }\n    );\n\n    this.request = request;\n  }\n\n  getId = () => {\n    return 'dida365';\n  };\n\n  getUserInfo = async () => {\n    const response = await this.request.get<Dida365Profile>('user/profile');\n    return {\n      name: response.name,\n      avatar: response.picture,\n      homePage: '',\n      description: response.username,\n    };\n  };\n\n  getTags = async (): Promise<string[]> => {\n    const dida365CheckResponse = await this.request.get<Dida365CheckResponse>(`batch/check/0`);\n    return dida365CheckResponse.tags.map((o) => o.name);\n  };\n\n  getRepositories = async (): Promise<Repository[]> => {\n    const dida365CheckResponse = await this.request.get<Dida365CheckResponse>(`batch/check/0`);\n    const groupMap = new Map<string, string>();\n    if (dida365CheckResponse.projectGroups) { // 检查 projectGroups 是否存在\n      dida365CheckResponse.projectGroups.forEach((group) => {\n        groupMap.set(group.id, group.name);\n      });\n    }\n\n    return dida365CheckResponse.projectProfiles\n      .filter((o) => !o.closed)\n      .map(({ id, name, groupId }) => ({\n        id: id,\n        name: name,\n        groupId: groupId\n          ? groupId\n          : localeService.format({\n              id: 'backend.services.dida365.rootGroup',\n              defaultMessage: 'Root',\n            }),\n        groupName: groupId\n          ? groupMap.get(groupId)!\n          : localeService.format({\n              id: 'backend.services.dida365.rootGroup',\n              defaultMessage: 'Root',\n            }),\n      }));\n  };\n\n  createDocument = async (request: Dida365CreateDocumentRequest): Promise<CompleteStatus> => {\n    const webRequestService = Container.get(IWebRequestService);\n\n    const header = await webRequestService.startChangeHeader({\n      urls: ['https://api.dida365.com/*'],\n      requestHeaders: [\n        {\n          name: 'origin',\n          value: 'https://dida365.com',\n        },\n      ],\n    });\n\n    const settings = await this.request.get<{ timeZone: string }>(\n      await webRequestService.changeUrl('user/preferences/settings?includeWeb=true', header)\n    );\n\n    const id = generateUuid().replace(/-/g, '').slice(0, 24);\n    const data = {\n      add: [\n        {\n          items: [],\n          reminders: [],\n          exDate: [],\n          dueDate: null,\n          priority: 0,\n          progress: 0,\n          assignee: null,\n          kind: 'NOTE',\n          sortOrder: -4611733297427382000,\n          startDate: null,\n          isFloating: false,\n          status: 0,\n          deleted: 0,\n          tags: request.tags,\n          projectId: request.repositoryId,\n          title: request.title,\n          content: request.content,\n          timeZone: settings.timeZone,\n          id: id,\n        },\n      ],\n      update: [],\n      delete: [],\n    };\n\n    await this.request.post(await webRequestService.changeUrl('batch/task', header), {\n      data: data,\n      headers: {\n        [header.name]: header.value,\n      },\n    });\n\n    await webRequestService.end(header);\n\n    return {\n      href: `https://dida365.com/#p/${request.repositoryId}/tasks/${id}`,\n    };\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/flomo/index.ts",
    "content": "import Service from './service';\n\nexport default () => {\n  return {\n    name: 'Flomo',\n    icon: 'flomo',\n    type: 'flomo',\n    service: Service,\n    homePage: 'https://flomoapp.com/',\n    permission: {\n      origins: ['https://flomoapp.com/*'],\n      permissions: ['cookies'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/flomo/service.ts",
    "content": "import { IBasicRequestService } from '@/service/common/request';\nimport { CompleteStatus } from 'common/backend/interface';\nimport Container from 'typedi';\nimport { DocumentService, CreateDocumentRequest } from '../../index';\nimport showdown from 'showdown';\nimport { ICookieService } from '@/service/common/cookie';\nimport locale from '@/common/locales';\n\nexport default class GithubDocumentService implements DocumentService {\n  getId = () => {\n    return 'Flomo';\n  };\n\n  getUserInfo = async () => {\n    return {\n      name: 'Flomo',\n      avatar: '',\n      homePage: 'https://flomoapp.com/',\n      description: 'Flomo',\n    };\n  };\n\n  getRepositories = async () => {\n    /**\n     * Check Login\n     */\n    await this.getXSRFToken();\n    return [\n      {\n        id: 'flomo',\n        name: 'Flomo',\n        groupId: 'flomo',\n        groupName: 'Flomo',\n      },\n    ];\n  };\n\n  createDocument = async (info: CreateDocumentRequest): Promise<CompleteStatus> => {\n    const request = Container.get(IBasicRequestService);\n    const converter = new showdown.Converter({});\n    converter.addExtension({\n      type: 'html',\n      filter: (html: string) => {\n        return html.replace(/<img src=\"(.+?)\"(.*)\\/>/g, '<p>$1</p>');\n      },\n    });\n    const XSRFToken = await this.getXSRFToken();\n    const res = await request.request<{ code: number; message: string }>(\n      'https://flomoapp.com/api/memo/',\n      {\n        method: 'put',\n        requestType: 'json',\n        headers: {\n          'x-requested-with': 'XMLHttpRequest',\n          'x-xsrf-token': decodeURIComponent(XSRFToken),\n        },\n        data: {\n          source: 'web',\n          parent_memo_slug: null,\n          content: converter.makeHtml(info.content),\n          file_ids: [],\n        },\n      }\n    );\n    if (res.code !== 0) {\n      throw new Error(res.message);\n    }\n    return {\n      href: `https://flomoapp.com/mine`,\n    };\n  };\n\n  private async getXSRFToken(): Promise<string> {\n    const cookies = await Container.get(ICookieService).get({\n      name: 'XSRF-TOKEN',\n      url: 'https://flomoapp.com/',\n    });\n    if (!cookies) {\n      throw new Error(\n        locale.format({\n          id: 'backend.services.flomo.login',\n        })\n      );\n    }\n    return cookies.value;\n  }\n}\n"
  },
  {
    "path": "src/common/backend/services/flowus/index.ts",
    "content": "import { ServiceMeta } from '@/common/backend';\nimport Service from './service';\n\nexport const flowusOrigin = 'https://flowus.cn';\n\nexport default (): ServiceMeta => {\n  return {\n    name: 'FlowUs息流',\n    icon: 'https://cdn.flowus.cn/icon.png',\n    type: 'flowus',\n    homePage: 'https://flowus.cn/',\n    service: Service,\n    permission: {\n      origins: [`${flowusOrigin}/*`, '<all_urls>'],\n      permissions: ['cookies'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/flowus/service.ts",
    "content": "import { CompleteStatus, UnauthorizedError } from '../interface';\nimport { DocumentService, CreateDocumentRequest } from '../../index';\nimport localeService from '@/common/locales';\nimport { extend, RequestMethod } from 'umi-request';\nimport { IWebRequestService, WebBlockHeader } from '@/service/common/webRequest';\nimport Container from 'typedi';\nimport { ICookieService } from '@/service/common/cookie';\nimport {\n  FlowUsToc,\n  FlowUsRepository,\n  FlowUsSpace,\n  FlowUsUserInfo,\n  Block,\n  OSSInfo,\n  TaskResult,\n  FlowUsResponse,\n  ROLE_WEIGHT,\n  Share,\n} from './type';\nimport { generateUuid } from '@web-clipper/shared/lib/uuid';\nimport { flowusOrigin } from '.';\nimport showdown from 'showdown';\nconst converter = new showdown.Converter({});\nexport default class FlowUsDocumentService implements DocumentService {\n  private request: RequestMethod;\n  private repositories: FlowUsRepository[];\n  private userSpaces?: FlowUsSpace;\n  private tocPageBlocks?: Record<string, Block>;\n  private userInfo?: FlowUsUserInfo;\n  private webRequestService: IWebRequestService;\n  private cookieService: ICookieService;\n\n  constructor() {\n    const request = extend({\n      prefix: `${flowusOrigin}/api/`,\n      timeout: 10000,\n      credentials: 'include',\n    });\n    this.request = request;\n    this.repositories = [];\n    this.webRequestService = Container.get(IWebRequestService);\n    this.cookieService = Container.get(ICookieService);\n\n    request.interceptors.response.use(\n      (response) => {\n        if (response.status === 401) {\n          throw new UnauthorizedError(\n            localeService.format({\n              id: 'backend.services.flowus.unauthorizedErrorMessage',\n              defaultMessage: 'Unauthorized! Please Login FlowUs Web.',\n            })\n          );\n        }\n        return response;\n      },\n      { global: false }\n    );\n  }\n\n  getId = () => {\n    return 'FlowUs';\n  };\n\n  getUserInfo = async () => {\n    if (!this.userInfo) {\n      const res = await this.fetchUserInfo();\n      this.userInfo = res.data;\n    }\n    const { nickname, avatar, ext } = this.userInfo;\n    return {\n      name: nickname,\n      avatar: avatar?.startsWith('http') ? avatar : getImageCdnUrl(avatar),\n      homePage: 'https://flowus.cn',\n      description: ext?.email?.email,\n    };\n  };\n\n  getRepositories = async () => {\n    if (!this.userInfo) {\n      const res = await this.fetchUserInfo();\n      this.userInfo = res.data;\n    }\n    if (!this.userSpaces) {\n      const res = await this.getUserSpaces();\n      this.userSpaces = res.data;\n    }\n\n    const { spaceViews, spaces } = this.userSpaces;\n\n    if (!spaceViews || !spaces) {\n      this.repositories = [];\n      return [];\n    }\n\n    const result: FlowUsRepository[] = [];\n    //拉取可用空间\n    const userSpaces = Object.values(spaceViews)\n      .filter((spaceView) => spaces[spaceView.spaceId])\n      .map((spaceView) => spaces[spaceView.spaceId]);\n\n    if (!this.tocPageBlocks) {\n      const allPromise = userSpaces.map((space) => {\n        return this.getSpaceRoot(space.uuid);\n      });\n      const allToc = await Promise.all(allPromise);\n      this.tocPageBlocks = allToc.reduce(\n        (pre, cur) => {\n          if (!cur.data.blocks) return pre;\n          Object.values(cur.data.blocks).forEach((b) => {\n            //保存所有的页面/多维表块\n            if ([0, 18, 19].includes(b.type)) {\n              pre[b.uuid] = b;\n            }\n          });\n          return pre;\n        },\n        {} as Record<string, Block>\n      );\n\n      userSpaces.forEach((sp) => {\n        sp.subNodes.forEach((id) => {\n          const block = this.tocPageBlocks?.[id];\n          if (!block) return;\n          if (block.permissions.some((o) => o.type === 'illegal')) return;\n          if (block.permissions.length === 0) return;\n          const { role } = getPermission(block, this.userInfo?.uuid!, sp.permissionGroups ?? []);\n          if (role === 'editor' || role === 'writer') {\n            //可保存到自页面的块\n            const spaceId = block.spaceId ?? sp.uuid;\n            let groupName = sp.title;\n            result.push({\n              id: block.uuid,\n              name: block.title || '未命名页面',\n              groupId: spaceId,\n              groupName,\n            });\n          }\n        });\n      });\n    }\n    this.repositories = result;\n    return result;\n  };\n\n  createDocument = async ({\n    repositoryId,\n    title,\n    content,\n  }: CreateDocumentRequest): Promise<CompleteStatus> => {\n    const repository = this.repositories.find((o) => o.id === repositoryId);\n    if (!repository) {\n      throw new Error('Illegal repository');\n    }\n    const documentId = await this.createEmptyPage(repository, title);\n    const html = `<!DOCTYPE html>\n    <html>\n      <head>\n        <title>${title}</title>\n      </head>\n      <body>\n       ${converter.makeHtml(`${content}`)}\n      </body>\n    </html>`;\n    const ossInfo = await this.requestWithCookie<FlowUsResponse<OSSInfo>>(async (header) => {\n      return this.request.post(\n        await this.webRequestService.changeUrl(`import_temp_file?source=web-clipper`, header),\n        {\n          headers: {\n            [header.name]: header.value,\n          },\n          data: {\n            content: html,\n            extName: 'html',\n          },\n        }\n      );\n    });\n    if (ossInfo.code !== 200) {\n      throw new Error('upload md content failed');\n    }\n    //导入\n    const res = await this.requestWithCookie<FlowUsResponse<{ taskId: string }>>(async (header) => {\n      return this.request.post(await this.webRequestService.changeUrl(`enqueueTask`, header), {\n        headers: {\n          [header.name]: header.value,\n        },\n        data: {\n          eventName: 'import',\n          request: {\n            blockId: documentId,\n            spaceId: repository.groupId,\n            importOptions: {\n              type: 'html',\n              ossName: ossInfo.data.ossName,\n            },\n          },\n        },\n      });\n    });\n    if (!res.data.taskId) {\n      throw new Error('enqueueTask failed');\n    }\n    const taskId = res.data.taskId;\n\n    const waitResult = async () => {\n      await sleep(2000);\n      const res = await this.requestWithCookie<FlowUsResponse<TaskResult>>(async (header) => {\n        return this.request.post(await this.webRequestService.changeUrl('getTasks', header), {\n          headers: {\n            [header.name]: header.value,\n          },\n          data: {\n            taskIds: [taskId],\n          },\n        });\n      });\n      if (res.code !== 200) {\n        throw new Error('getTasks failed');\n      }\n      const result = res.data.results[taskId];\n      if (result && result.status === 'success') {\n        if (result.result?.status === 'success') {\n          //do nothing\n        } else if (result.result?.msg) {\n          throw new Error(result.result?.msg);\n        }\n      } else {\n        await waitResult();\n      }\n    };\n    await waitResult();\n    this.changeTitle(documentId, repository.groupId, title);\n    return {\n      href: `${flowusOrigin}/${documentId}`,\n    };\n  };\n\n  createEmptyPage = async (repository: FlowUsRepository, title: string) => {\n    if (!this.tocPageBlocks) {\n      throw new Error('Illegal tocBlocks');\n    }\n    const documentId = generateUuid();\n    const parentId = repository.id;\n    const spaceId = repository.groupId;\n    const blocks = this.tocPageBlocks;\n    if (!blocks) {\n      throw new Error('Illegal blocks');\n    }\n    const subNodes = blocks[parentId].subNodes;\n    const after = subNodes[subNodes.length - 1];\n\n    const operations = {\n      requestId: generateUuid(),\n      transactions: [\n        {\n          id: generateUuid(),\n          spaceId,\n          operations: [\n            {\n              id: documentId,\n              path: [],\n              command: 'set',\n              table: 'block',\n              args: {\n                uuid: documentId,\n                spaceId,\n                parentId,\n                textColor: '',\n                backgroundColor: '',\n                type: 0,\n                status: 1,\n                permissions: [],\n                updateBy: this.userInfo?.uuid,\n                updateAt: Date.now(),\n                data: {\n                  segments: [{ type: 0, text: title, enhancer: {} }],\n                },\n              },\n            },\n            {\n              id: parentId,\n              command: 'listAfter',\n              path: ['subNodes'],\n              table: 'block',\n              args: {\n                uuid: documentId,\n                after,\n              },\n            },\n          ],\n        },\n      ],\n    };\n    await this.requestWithCookie(async (header) => {\n      return this.request.post(\n        await this.webRequestService.changeUrl('blocks/transactions', header),\n        {\n          data: operations,\n          headers: {\n            [header.name]: header.value,\n          },\n        }\n      );\n    });\n    return documentId;\n  };\n  private changeTitle = async (documentId: string, spaceId: string, title: string) => {\n    const operations = {\n      requestId: generateUuid(),\n      transactions: [\n        {\n          id: generateUuid(),\n          spaceId,\n          operations: [\n            {\n              id: documentId,\n              path: ['data'],\n              command: 'update',\n              table: 'block',\n              args: {\n                segments: [{ type: 0, text: title, enhancer: {} }],\n              },\n            },\n          ],\n        },\n      ],\n    };\n    await this.requestWithCookie(async (header) => {\n      return this.request.post(\n        await this.webRequestService.changeUrl('blocks/transactions', header),\n        {\n          data: operations,\n          headers: {\n            [header.name]: header.value,\n          },\n        }\n      );\n    });\n  };\n\n  private getUserSpaces = async () => {\n    return this.requestWithCookie<FlowUsResponse<FlowUsSpace>>(async (header) => {\n      return this.request.get(\n        await this.webRequestService.changeUrl(`users/${this.userInfo?.uuid}/root`, header),\n        {\n          headers: {\n            [header.name]: header.value,\n          },\n        }\n      );\n    });\n  };\n  private getSpaceRoot = async (spaceId: string) => {\n    return this.requestWithCookie<FlowUsToc>(async (header) => {\n      return this.request.get<FlowUsToc>(\n        await this.webRequestService.changeUrl(`spaces/${spaceId}/root`, header)\n      );\n    });\n  };\n\n  private fetchUserInfo = async () => {\n    return this.requestWithCookie<FlowUsResponse<FlowUsUserInfo>>(async (header) => {\n      return this.request.get(await this.webRequestService.changeUrl('users/me', header));\n    });\n  };\n\n  /**\n   * Modify the cookie when request\n   */\n  private requestWithCookie = async <T>(\n    requestFunction: (header: WebBlockHeader) => Promise<T>\n  ) => {\n    const cookies = await this.cookieService.getAll({\n      url: flowusOrigin,\n    });\n    const cookieString = cookies.map((o) => `${o.name}=${o.value}`).join(';');\n    const header = await this.webRequestService.startChangeHeader({\n      urls: [`${flowusOrigin}*`],\n      requestHeaders: [\n        {\n          name: 'cookie',\n          value: cookieString,\n        },\n      ],\n    });\n    try {\n      const result = await requestFunction(header);\n      await this.webRequestService.end(header);\n      return result;\n    } catch (error) {\n      await this.webRequestService.end(header);\n      throw error;\n    }\n  };\n}\n\nconst compressImageSupport = /^(jpg|jpeg|png|bmp|webp|tiff)$/i;\nfunction getImageCdnUrl(ossName?: string) {\n  if (!ossName) return '';\n  const index = ossName.lastIndexOf('.');\n  const extName = ossName.substring(index + 1);\n  let imgProcess = '';\n  if (compressImageSupport.test(extName.toLocaleLowerCase())) {\n    imgProcess = `img_process=/resize,w_${500 * Math.ceil(window.devicePixelRatio)}/quality,q_80/`;\n  }\n  return `https://cdn2.flowus.cn/${ossName}?${imgProcess}`;\n}\nconst sleep = (durationInMs: number): Promise<void> => {\n  return new Promise((resolve) => {\n    setTimeout(resolve, durationInMs);\n  });\n};\n\nconst getPermission = (block: Block, userId: string, permissionGroups: any[]) => {\n  const data: Share = {\n    shared: false,\n    illegal: false,\n    isRestricted: false,\n    allowDuplicate: true,\n    permissions: [],\n    role: 'none',\n    roleWithoutPublic: 'none',\n  };\n  if (block.permissions.length) {\n    const getBiggerRole = (\n      type: keyof Block['permissions'][0],\n      value: any,\n      role?: keyof typeof ROLE_WEIGHT\n    ) => {\n      const permissions = block.permissions.find((p) => p[type] === value);\n      if (\n        permissions &&\n        role &&\n        permissions.role &&\n        ROLE_WEIGHT[permissions.role] > ROLE_WEIGHT[role]\n      ) {\n        return permissions;\n      }\n    };\n    const newPermissions = block.permissions\n      .filter((o) => {\n        return o.type !== 'illegal' && o.type !== 'restricted';\n      })\n      .map((o) => {\n        if (o.type === 'space') {\n          return getBiggerRole('type', o.type, o.role) || o;\n        }\n        if (o.type === 'group') {\n          return getBiggerRole('groupId', o.groupId, o.role) || o;\n        }\n        if (o.type === 'user') {\n          return getBiggerRole('userId', o.userId, o.role) || o;\n        }\n        return o;\n      });\n    const diffPermissions = block.permissions.filter((o) => {\n      if (o.type === 'illegal' || o.type === 'restricted') {\n        return false;\n      }\n      if (o.type === 'space' || o.type === 'public') {\n        return newPermissions.every((p) => p.type !== o.type);\n      }\n      if (o.type === 'group') {\n        return newPermissions.every((p) => p.groupId !== o.groupId);\n      }\n      return newPermissions.every((p) => p.userId !== o.userId);\n    });\n    data.permissions = [...newPermissions, ...diffPermissions];\n    const ownPermission = data.permissions.find((p) => p.userId === userId);\n    const groupPermissions = data.permissions.filter((p) => {\n      const group = permissionGroups?.find((g) => g.id === p.groupId);\n      return group?.userIds.includes(userId);\n    });\n    const allPermissions = [ownPermission, ...groupPermissions];\n    const spacePermission = block.permissions.find((p) => p.type === 'space');\n    allPermissions.push(spacePermission);\n    data.roleWithoutPublic = allPermissions.reduce(\n      (pre: keyof typeof ROLE_WEIGHT, permission: Block['permissions'][0] | undefined) => {\n        if (!permission) return pre;\n        const role = permission.role ?? 'none';\n        return ROLE_WEIGHT[role] > ROLE_WEIGHT[pre] ? role : pre;\n      },\n      'none'\n    );\n    data.role = data.roleWithoutPublic;\n  }\n\n  return data;\n};\n"
  },
  {
    "path": "src/common/backend/services/flowus/type.ts",
    "content": "import { Repository } from '../interface';\n\ninterface Space {\n  uuid: string;\n  title: string;\n  subNodes: string[];\n  permissionGroups?: any[];\n}\ninterface SpaceView {\n  uuid: string;\n  spaceId: string;\n  title: string;\n}\ntype BlockType = number;\nexport interface Block {\n  uuid: string;\n  spaceId: string;\n  parentId: string;\n  type: BlockType;\n  title: string;\n  subNodes: string[];\n  permissions: {\n    type: string;\n    role?: keyof typeof ROLE_WEIGHT;\n    userId?: string;\n    groupId?: string;\n  }[];\n}\nexport interface FlowUsSpace {\n  spaces: Record<string, Space>;\n  spaceViews: Record<string, SpaceView>;\n}\nexport interface OSSInfo {\n  ossName: string;\n}\nexport interface FlowUsToc {\n  data: {\n    blocks?: Record<string, Block>;\n  };\n}\n\nexport interface FlowUsUserInfo {\n  uuid: string;\n  phone: string;\n  nickname: string;\n  backgroundColor: string;\n  spaceViews: string;\n  avatar?: string;\n  ext?: {\n    email?: {\n      id: string;\n      email: string;\n    };\n  };\n}\n\nexport interface FlowUsRepository extends Repository {}\n\nexport interface TaskResult {\n  results: Record<\n    string,\n    {\n      taskId: string;\n      eventName: string;\n      status: string;\n      result?: {\n        status?: string;\n        url?: string;\n        size?: number;\n        ossName?: string;\n        uuid?: string;\n        msg?: string;\n      };\n    }\n  >;\n}\n\nexport interface FlowUsResponse<DATA> {\n  msg: string;\n  code: number;\n  data: DATA;\n}\n\nexport const ROLE_WEIGHT = {\n  none: 0,\n  reader: 1,\n  writer: 2,\n  editor: 3,\n  commenter: 4,\n};\n\nexport interface Share {\n  shared: boolean;\n  title?: string;\n  illegal: boolean;\n  parentId?: string;\n  isRestricted: boolean;\n  /** 允许复制、打印、下载 */\n  allowDuplicate: boolean;\n  permissions: Block['permissions'];\n  role: keyof typeof ROLE_WEIGHT;\n  roleWithoutPublic: keyof typeof ROLE_WEIGHT;\n}\n"
  },
  {
    "path": "src/common/backend/services/github/form.tsx",
    "content": "import { KeyOutlined } from '@ant-design/icons';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input, Select, Tooltip } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport { GithubBackendServiceConfig } from './interface';\nimport { FormattedMessage } from 'react-intl';\nimport locale from '@/common/locales';\nimport { stringify } from 'qs';\n\ninterface GithubFormProps {\n  verified?: boolean;\n  info?: GithubBackendServiceConfig;\n}\n\nconst GenerateNewTokenUrl = `https://github.com/settings/tokens/new?${stringify({\n  scopes: 'repo',\n  description: 'Web Clipper',\n})}`;\n\nconst visibilityOptions = [\n  {\n    label: <FormattedMessage id=\"backend.services.github.form.visibility.all\" />,\n    value: 'all',\n  },\n  {\n    label: (\n      <FormattedMessage\n        id=\"backend.services.github.form.visibility.public\"\n        defaultMessage=\"Public\"\n      />\n    ),\n    value: 'public',\n  },\n  {\n    label: (\n      <FormattedMessage\n        id=\"backend.services.github.form.visibility.private\"\n        defaultMessage=\"Private\"\n      />\n    ),\n    value: 'private',\n  },\n];\n\nconst GithubForm: React.FC<GithubFormProps & FormComponentProps> = ({\n  form: { getFieldDecorator },\n  info,\n  verified,\n}) => {\n  const disabled = verified || !!info;\n  let initAccessToken;\n  let visibility;\n  if (info) {\n    initAccessToken = info.accessToken;\n    visibility = info.visibility;\n  }\n  return (\n    <Fragment>\n      <Form.Item\n        label={\n          <FormattedMessage\n            id=\"backend.services.github.form.visibility\"\n            defaultMessage=\"Visibility\"\n          />\n        }\n      >\n        {getFieldDecorator('visibility', {\n          initialValue: visibility,\n        })(\n          <Select allowClear>\n            {visibilityOptions.map(o => (\n              <Select.Option value={o.value} key={o.value}>\n                {o.label}\n              </Select.Option>\n            ))}\n          </Select>\n        )}\n      </Form.Item>\n      <Form.Item label=\"AccessToken\">\n        {getFieldDecorator('accessToken', {\n          initialValue: initAccessToken,\n          rules: [\n            {\n              required: true,\n              message: (\n                <FormattedMessage\n                  id=\"backend.services.github.accessToken.message\"\n                  defaultMessage=\"AccessToken is required\"\n                />\n              ),\n            },\n          ],\n        })(\n          <Input\n            disabled={disabled}\n            suffix={\n              <Tooltip\n                title={\n                  <span\n                    style={{\n                      whiteSpace: 'nowrap',\n                    }}\n                  >\n                    {locale.format({\n                      id: 'backend.services.github.form.GenerateNewToken',\n                      defaultMessage: 'Generate new token',\n                    })}\n                  </span>\n                }\n              >\n                <a href={GenerateNewTokenUrl} target={GenerateNewTokenUrl}>\n                  <KeyOutlined />\n                </a>\n              </Tooltip>\n            }\n          />\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default GithubForm;\n"
  },
  {
    "path": "src/common/backend/services/github/headerForm.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Select, Badge } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport backend from '../..';\nimport GithubDocumentService from './service';\nimport locale from '@/common/locales';\nimport { useFetch } from '@shihengtech/hooks';\n\nconst HeaderForm: React.FC<FormComponentProps & { currentRepository: any }> = ({\n  form: { getFieldDecorator },\n  currentRepository,\n}) => {\n  const service = backend.getDocumentService() as GithubDocumentService;\n  // eslint-disable-next-line react-hooks/rules-of-hooks\n  const labelsResponse = useFetch(\n    async () => {\n      if (currentRepository) {\n        return service.getRepoLabels(currentRepository);\n      }\n      return [];\n    },\n    [currentRepository, service],\n    {\n      initialState: {\n        data: [],\n      },\n    }\n  );\n\n  return (\n    <Fragment>\n      <Form.Item>\n        {getFieldDecorator('labels')(\n          <Select\n            mode=\"tags\"\n            maxTagCount={3}\n            style={{ width: '100%' }}\n            placeholder={locale.format({\n              id: 'backend.services.github.headerForm.applyLabels',\n              defaultMessage: 'Apply labels',\n            })}\n            loading={labelsResponse.loading}\n          >\n            {labelsResponse.data?.map(o => (\n              <Select.Option key={o.name} value={o.name} title={o.description}>\n                <Badge color={`#${o.color}`} text={o.name} />\n              </Select.Option>\n            ))}\n          </Select>\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default HeaderForm;\n"
  },
  {
    "path": "src/common/backend/services/github/index.ts",
    "content": "import { ServiceMeta } from './../interface';\r\nimport Service from './service';\r\nimport Form from './form';\r\nimport headerForm from './headerForm';\r\n\r\nexport default () => {\r\n  return {\r\n    name: 'Github',\r\n    icon: 'github',\r\n    type: 'github',\r\n    service: Service,\r\n    form: Form,\r\n    headerForm: headerForm,\r\n    homePage: 'https://github.com/',\r\n    permission: {\r\n      origins: ['https://api.github.com/*'],\r\n    },\r\n  } as ServiceMeta;\r\n};\r\n"
  },
  {
    "path": "src/common/backend/services/github/interface.ts",
    "content": "import { Repository, CreateDocumentRequest } from '../interface';\n\nexport interface GithubBackendServiceConfig {\n  accessToken: string;\n  visibility: string;\n}\n\nexport interface GithubCreateDocumentRequest extends CreateDocumentRequest {\n  labels: string[];\n}\n\nexport interface GithubUserInfoResponse {\n  avatar_url: string;\n  name: string;\n  bio: string;\n  html_url: string;\n}\n\nexport interface GithubRepository extends Repository {\n  namespace: string;\n}\n\nexport interface GithubRepositoryResponse {\n  id: number;\n  name: string;\n  full_name: string;\n  created_at: string;\n  description: string;\n  private: boolean;\n}\n\nexport interface GithubLabel {\n  color: string;\n  description: string;\n  name: string;\n  default: boolean;\n}\n"
  },
  {
    "path": "src/common/backend/services/github/service.ts",
    "content": "import { CompleteStatus } from 'common/backend/interface';\nimport {\n  GithubBackendServiceConfig,\n  GithubUserInfoResponse,\n  GithubRepositoryResponse,\n  GithubRepository,\n  GithubLabel,\n  GithubCreateDocumentRequest,\n} from './interface';\nimport { DocumentService } from '../../index';\nimport axios, { AxiosInstance } from 'axios';\nimport md5 from '@web-clipper/shared/lib/md5';\nimport { stringify } from 'qs';\n\nconst PAGE_LIMIT = 100;\n\nexport default class GithubDocumentService implements DocumentService {\n  private request: AxiosInstance;\n  private repositories: GithubRepository[];\n  private config: GithubBackendServiceConfig;\n\n  constructor(config: GithubBackendServiceConfig) {\n    const request = axios.create({\n      baseURL: 'https://api.github.com',\n      headers: {\n        Accept: 'application/vnd.github.v3+json',\n        Authorization: `token ${config.accessToken}`,\n      },\n      timeout: 10000,\n      transformResponse: [\n        (data): string => {\n          return JSON.parse(data);\n        },\n      ],\n      withCredentials: true,\n    });\n    this.request = request;\n    this.repositories = [];\n    this.config = config;\n  }\n\n  getId = () => {\n    return md5(this.config.accessToken);\n  };\n\n  getUserInfo = async () => {\n    const data = await this.request.get<GithubUserInfoResponse>('user');\n    const { name, avatar_url: avatar, html_url: homePage, bio: description } = data.data;\n    return {\n      name,\n      avatar,\n      homePage,\n      description,\n    };\n  };\n\n  getRepositories = async (): Promise<GithubRepository[]> => {\n    let page = 1;\n    let foo = await this.getGithubRepositories({ page, visibility: this.config.visibility });\n    let result: GithubRepository[] = [];\n    result = result.concat(foo);\n    while (foo.length === PAGE_LIMIT) {\n      page++;\n      foo = await this.getGithubRepositories({ page, visibility: this.config.visibility });\n      result = result.concat(foo);\n    }\n    this.repositories = result;\n    return result;\n  };\n\n  createDocument = async (info: GithubCreateDocumentRequest): Promise<CompleteStatus> => {\n    if (!this.repositories) {\n      this.getRepositories();\n    }\n    const { content: body, title, repositoryId, labels } = info;\n    const repository = this.repositories.find(o => o.id === repositoryId);\n    if (!repository) {\n      throw new Error('can not find repository');\n    }\n\n    const response = await this.request.post<{\n      html_url: string;\n      id: number;\n    }>(`/repos/${repository.namespace}/issues`, {\n      title,\n      body,\n      labels,\n    });\n    return {\n      href: response.data.html_url,\n    };\n  };\n\n  getRepoLabels = async (repo: GithubRepository): Promise<GithubLabel[]> => {\n    return (await this.request.get<GithubLabel[]>(`/repos/${repo.namespace}/labels`)).data;\n  };\n\n  private getGithubRepositories = async ({\n    page,\n    visibility,\n  }: {\n    page: number;\n    visibility: string;\n  }) => {\n    const response = await this.request.get<GithubRepositoryResponse[]>(\n      `user/repos?${stringify({ page, per_page: PAGE_LIMIT, visibility })}`\n    );\n    const repositories = response.data;\n    return repositories.map(\n      (repository): GithubRepository => {\n        const { id, name, full_name: namespace } = repository;\n        return {\n          id: id.toString(),\n          name,\n          namespace,\n          groupId: namespace.split('/')[0],\n          groupName: namespace.split('/')[0],\n        };\n      }\n    );\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/github_repository/form.tsx",
    "content": "import { KeyOutlined } from '@ant-design/icons';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input, Select, Tooltip } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport { GithubBackendServiceConfig } from './interface';\nimport { FormattedMessage } from 'react-intl';\nimport locale from '@/common/locales';\nimport { stringify } from 'qs';\n\ninterface GithubFormProps {\n  verified?: boolean;\n  info?: GithubBackendServiceConfig;\n}\n\nconst GenerateNewTokenUrl = `https://github.com/settings/tokens/new?${stringify({\n  scopes: 'repo',\n  description: 'Web Clipper',\n})}`;\n\nconst visibilityOptions = [\n  {\n    label: <FormattedMessage id=\"backend.services.github.form.visibility.all\" />,\n    value: 'all',\n  },\n  {\n    label: <FormattedMessage id=\"backend.services.github.form.visibility.public\" />,\n    value: 'public',\n  },\n  {\n    label: <FormattedMessage id=\"backend.services.github.form.visibility.private\" />,\n    value: 'private',\n  },\n];\n\nconst GithubForm: React.FC<GithubFormProps & FormComponentProps> = ({\n  form: { getFieldDecorator },\n  info,\n  verified,\n}) => {\n  const disabled = verified || !!info;\n  let initAccessToken;\n  let visibility;\n  let savePath;\n  if (info) {\n    initAccessToken = info.accessToken;\n    visibility = info.visibility;\n    savePath = info.savePath;\n  }\n  return (\n    <Fragment>\n      <Form.Item label={<FormattedMessage id=\"backend.services.github.form.visibility\" />}>\n        {getFieldDecorator('visibility', {\n          initialValue: visibility,\n        })(\n          <Select allowClear>\n            {visibilityOptions.map(o => (\n              <Select.Option value={o.value} key={o.value}>\n                {o.label}\n              </Select.Option>\n            ))}\n          </Select>\n        )}\n      </Form.Item>\n      <Form.Item label=\"AccessToken\">\n        {getFieldDecorator('accessToken', {\n          initialValue: initAccessToken,\n          rules: [\n            {\n              required: true,\n              message: (\n                <FormattedMessage\n                  id=\"backend.services.github.accessToken.message\"\n                  defaultMessage=\"AccessToken is required\"\n                />\n              ),\n            },\n          ],\n        })(\n          <Input\n            disabled={disabled}\n            suffix={\n              <Tooltip\n                title={\n                  <span\n                    style={{\n                      whiteSpace: 'nowrap',\n                    }}\n                  >\n                    {locale.format({\n                      id: 'backend.services.github.form.GenerateNewToken',\n                      defaultMessage: 'Generate new token',\n                    })}\n                  </span>\n                }\n              >\n                <a href={GenerateNewTokenUrl} target={GenerateNewTokenUrl}>\n                  <KeyOutlined />\n                </a>\n              </Tooltip>\n            }\n          />\n        )}\n      </Form.Item>\n      <Form.Item\n        label={\n          <FormattedMessage\n            id=\"backend.services.github.form.storageLocation.code.savePath\"\n            defaultMessage=\"Save Path\"\n          />\n        }\n      >\n        {getFieldDecorator('savePath', {\n          initialValue: savePath,\n          rules: [\n            {\n              required: false,\n            },\n            {\n              validator: (_r: unknown, value: string, callback: (message?: string) => {}) => {\n                if (typeof value === 'string') {\n                  if (value.startsWith('/')) {\n                    return callback('path cannot start with a slash');\n                  }\n                }\n                return callback();\n              },\n            },\n          ],\n        })(\n          <Input\n            placeholder={locale.format({\n              id: 'backend.services.github.form.storageLocation.code.savePathPlaceHolder',\n              defaultMessage: 'Only takes effect when saving to code.',\n            })}\n          />\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default GithubForm;\n"
  },
  {
    "path": "src/common/backend/services/github_repository/index.ts",
    "content": "import { ServiceMeta } from '../interface';\r\nimport Service from './service';\r\nimport Form from './form';\r\n\r\nexport default () => {\r\n  return {\r\n    name: 'Github Repository',\r\n    icon: 'github_repository',\r\n    type: 'github_repository',\r\n    service: Service,\r\n    form: Form,\r\n    homePage: 'https://github.com/',\r\n    permission: {\r\n      origins: ['https://api.github.com/*'],\r\n    },\r\n  } as ServiceMeta;\r\n};\r\n"
  },
  {
    "path": "src/common/backend/services/github_repository/interface.ts",
    "content": "import { Repository, CreateDocumentRequest } from '../interface';\n\nexport interface GithubBackendServiceConfig {\n  accessToken: string;\n  visibility: string;\n  storageLocation: string;\n  savePath: string;\n}\n\nexport interface GithubCreateDocumentRequest extends CreateDocumentRequest {\n  labels: string[];\n}\n\nexport interface GithubUserInfoResponse {\n  avatar_url: string;\n  name: string;\n  bio: string;\n  html_url: string;\n}\n\nexport interface GithubRepository extends Repository {\n  namespace: string;\n}\n\nexport interface GithubRepositoryResponse {\n  id: number;\n  name: string;\n  full_name: string;\n  created_at: string;\n  description: string;\n  private: boolean;\n}\n"
  },
  {
    "path": "src/common/backend/services/github_repository/service.ts",
    "content": "import { CompleteStatus } from 'common/backend/interface';\nimport {\n  GithubBackendServiceConfig,\n  GithubUserInfoResponse,\n  GithubRepositoryResponse,\n  GithubRepository,\n  GithubCreateDocumentRequest,\n} from './interface';\nimport { DocumentService } from '../../index';\nimport axios, { AxiosInstance } from 'axios';\nimport md5 from '@web-clipper/shared/lib/md5';\nimport { stringify } from 'qs';\nimport { isUndefined, toNumber } from 'lodash';\nimport fileNamify from 'filenamify';\n\nconst PAGE_LIMIT = 100;\n\nexport default class GithubRepositoryDocumentService implements DocumentService {\n  private request: AxiosInstance;\n  private repositories: GithubRepository[];\n  private config: GithubBackendServiceConfig;\n\n  constructor(config: GithubBackendServiceConfig) {\n    const request = axios.create({\n      baseURL: 'https://api.github.com',\n      headers: {\n        Accept: 'application/vnd.github.v3+json',\n        Authorization: `token ${config.accessToken}`,\n      },\n      timeout: 10000,\n      transformResponse: [\n        (data): string => {\n          return JSON.parse(data);\n        },\n      ],\n      withCredentials: true,\n    });\n    this.request = request;\n    this.repositories = [];\n    this.config = config;\n  }\n\n  getId = () => {\n    return md5(`${this.config.accessToken}_github_repository`);\n  };\n\n  getStorageLocation = () => {\n    return this.config.storageLocation;\n  };\n\n  getUserInfo = async () => {\n    const data = await this.request.get<GithubUserInfoResponse>('user');\n    const { name, avatar_url: avatar, html_url: homePage, bio: description } = data.data;\n    return {\n      name,\n      avatar,\n      homePage,\n      description,\n    };\n  };\n\n  getRepositories = async (): Promise<GithubRepository[]> => {\n    let page = 1;\n    let foo = await this.getGithubRepositories({ page, visibility: this.config.visibility });\n    let result: GithubRepository[] = [];\n    result = result.concat(foo);\n    while (foo.length === PAGE_LIMIT) {\n      page++;\n      foo = await this.getGithubRepositories({ page, visibility: this.config.visibility });\n      result = result.concat(foo);\n    }\n    this.repositories = result;\n    return result;\n  };\n\n  createDocument = async (info: GithubCreateDocumentRequest): Promise<CompleteStatus> => {\n    if (!this.repositories) {\n      this.getRepositories();\n    }\n    const { content: body, title, repositoryId } = info;\n    const repository = this.repositories.find(o => o.id === repositoryId);\n    if (!repository) {\n      throw new Error('can not find repository');\n    }\n\n    if (isUndefined(this.config.savePath)) this.config.savePath = '';\n    if (this.config.savePath.startsWith('/')) this.config.savePath.substr(1);\n    if (!this.config.savePath.endsWith('/') && this.config.savePath.length > 0)\n      this.config.savePath += '/';\n\n    let b64EncodeUnicode = (str: string) => {\n      return btoa(\n        encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function toSolidBytes(_match, p1) {\n          return String.fromCharCode(toNumber(`0x${p1}`));\n        })\n      );\n    };\n    let fileContent: string = b64EncodeUnicode(`# ${title}\\n${body}`);\n\n    let fileName: string = fileNamify(title, { replacement: ' ' });\n\n    let requestPath: string = `/repos/${repository.namespace}/contents/${this.config.savePath}${fileName}.md`;\n\n    const response = await this.request\n      .put<{\n        content: { html_url: string };\n      }>(requestPath, {\n        message: `Clip \"${title}\"`,\n        content: fileContent,\n      })\n      .catch(error => {\n        if (error.response) {\n          if (error.response.status === 422)\n            throw new Error('Response Status: 422. The file may already exist.');\n        } else if (error.request) {\n          throw new Error(error.request);\n        } else {\n          throw new Error(error.message);\n        }\n      });\n\n    return response ? { href: response.data.content.html_url } : {};\n  };\n\n  private getGithubRepositories = async ({\n    page,\n    visibility,\n  }: {\n    page: number;\n    visibility: string;\n  }) => {\n    const response = await this.request.get<GithubRepositoryResponse[]>(\n      `user/repos?${stringify({ page, per_page: PAGE_LIMIT, visibility })}`\n    );\n    const repositories = response.data;\n    return repositories.map(\n      (repository): GithubRepository => {\n        const { id, name, full_name: namespace } = repository;\n        return {\n          id: id.toString(),\n          name,\n          namespace,\n          groupId: namespace.split('/')[0],\n          groupName: namespace.split('/')[0],\n        };\n      }\n    );\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/interface.ts",
    "content": "export interface CreateDocumentRequest {\n  title: string;\n  content: string;\n  url?: string;\n  repositoryId: string;\n}\n\nexport interface CompleteStatus {\n  href?: string;\n}\n\nexport interface UserInfo {\n  name: string;\n  avatar: string;\n  homePage?: string;\n  description?: string;\n}\n\nexport interface Repository {\n  /**\n   * 仓库 ID\n   */\n  id: string;\n  /**\n   * 仓库名\n   */\n  name: string;\n  /**\n   * 团队 ID\n   */\n  groupId: string;\n  /**\n   * 团队 名称\n   */\n  groupName: string;\n  disabled?: boolean;\n}\n\nexport interface ServiceMeta {\n  /**\n   * Name of Backend Service\n   */\n  name: string;\n  /**\n   * icon\n   */\n  icon: string;\n  /**\n   * Type of Backend Service\n   */\n  type: string;\n  /**\n   * Backend Service\n   */\n  service: Type<DocumentService>;\n  /**\n   * 主页\n   */\n  homePage?: string;\n  /**\n   * 配置表单\n   */\n  form?: any;\n  complete?: any;\n  oauthUrl?: string;\n  headerForm?: any;\n  permission?: chrome.permissions.Permissions;\n}\n\nexport interface UpdateTOCRequest {}\n\nexport interface DocumentService<T = any> {\n  getId(): string;\n\n  getRepositories(): Promise<Repository[]>;\n\n  createDocument(request: T): Promise<CompleteStatus | void>;\n\n  getUserInfo(): Promise<UserInfo>;\n\n  refreshToken?(info: T): Promise<T>;\n}\n\ninterface ErrorOptions {\n  message: string;\n}\n\nclass BaseError<T extends ErrorOptions> extends Error {\n  protected options?: T;\n\n  constructor(options?: T) {\n    super();\n    this.options = options || ({} as T);\n    this.message = this.options.message || '';\n    this.name = this.constructor.name;\n  }\n\n  public static from(err: Error): BaseError<ErrorOptions> {\n    const ErrorClass = this;\n    const newErr = new ErrorClass<ErrorOptions>();\n    newErr.message = err.message;\n    newErr.stack = err.stack;\n    return newErr;\n  }\n}\n\ninterface HttpErrorOptions extends ErrorOptions {\n  status: number;\n}\n\nexport class HttpError extends BaseError<HttpErrorOptions> {\n  public status: number;\n  protected options: HttpErrorOptions;\n\n  constructor(options: HttpErrorOptions) {\n    super(options);\n    this.options = options;\n    this.status = this.options.status;\n  }\n}\n\nexport class UnauthorizedError extends HttpError {\n  constructor(message?: string) {\n    const status = 401;\n    super({ message: message || 'Unauthorized', status });\n  }\n}\n"
  },
  {
    "path": "src/common/backend/services/joplin/form.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input, Checkbox } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport { JoplinBackendServiceConfig } from '../../clients/joplin';\nimport { FormattedMessage } from 'react-intl';\n\ninterface FormProps extends FormComponentProps {\n  verified?: boolean;\n  info?: JoplinBackendServiceConfig;\n}\n\nconst InitForm: React.FC<FormProps> = ({ form: { getFieldDecorator }, info }) => {\n  return (\n    <Fragment>\n      <Form.Item label=\"Authorization token\">\n        {getFieldDecorator('token', {\n          initialValue: info?.token,\n          rules: [\n            {\n              required: true,\n              message: 'Authorization token is required!',\n            },\n          ],\n        })(<Input.TextArea autoSize />)}\n      </Form.Item>\n      <Form.Item label={<FormattedMessage id=\"backend.services.joplin.filter_tags\" />}>\n        {getFieldDecorator('filterTags', {\n          initialValue: info?.filterTags ?? false,\n          valuePropName: 'checked',\n        })(\n          <Checkbox>\n            <FormattedMessage id=\"backend.services.joplin.filter_unused_tags\" />\n          </Checkbox>\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default InitForm;\n"
  },
  {
    "path": "src/common/backend/services/joplin/headerForm.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Select } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport backend from '../..';\nimport { useFetch } from '@shihengtech/hooks';\nimport JoplinDocumentService from './service';\nimport locale from '@/common/locales';\n\nconst HeaderForm: React.FC<FormComponentProps> = ({ form: { getFieldDecorator } }) => {\n  const service = backend.getDocumentService() as JoplinDocumentService;\n  const tagResponse = useFetch(async () => service.getTags(), [service], {\n    initialState: {\n      data: [],\n    },\n  });\n\n  return (\n    <Fragment>\n      <Form.Item>\n        {getFieldDecorator('tags', {\n          initialValue: [],\n        })(\n          <Select\n            mode=\"tags\"\n            maxTagCount={3}\n            style={{ width: '100%' }}\n            placeholder={locale.format({\n              id: 'backend.services.joplin.headerForm.tags',\n            })}\n            loading={tagResponse.loading}\n          >\n            {tagResponse.data?.map(o => (\n              <Select.Option key={o.id} value={o.title} title={o.title}>\n                {o.title}\n              </Select.Option>\n            ))}\n          </Select>\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default HeaderForm;\n"
  },
  {
    "path": "src/common/backend/services/joplin/index.ts",
    "content": "import { ServiceMeta } from './../interface';\nimport Service from './service';\nimport Form from './form';\nimport localeService from '@/common/locales';\nimport headerForm from './headerForm';\n\nexport default (): ServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.joplin.name',\n    }),\n    icon: 'joplin',\n    type: 'joplin',\n    service: Service,\n    headerForm,\n    form: Form,\n    homePage: 'https://joplinapp.org/',\n    permission: {\n      origins: ['http://localhost:41184/*'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/joplin/service.ts",
    "content": "import { RequestHelper } from '@/service/request/common/request';\nimport { IBasicRequestService } from './../../../../service/common/request';\nimport { Container } from 'typedi';\nimport { DocumentService } from './../../index';\n\nimport {\n  LegacyJoplinClient,\n  JoplinClient,\n  JoplinBackendServiceConfig,\n  JoplinCreateDocumentRequest,\n  IJoplinClient,\n} from '../../clients/joplin';\n\nconst HOST = 'http://localhost:41184/';\n\nexport default class JoplinDocumentService implements DocumentService {\n  private client?: Promise<IJoplinClient>;\n  constructor(private config: JoplinBackendServiceConfig) {}\n\n  getId() {\n    return this.config.token;\n  }\n\n  getUserInfo = async () => {\n    return {\n      name: `Joplin`,\n      avatar: '',\n      homePage: 'https://joplinapp.org/',\n      description: `Save to Joplin`,\n    };\n  };\n\n  createDocument = async (data: JoplinCreateDocumentRequest) => {\n    const joplinClient = await this.getJoplinClient();\n    return joplinClient.createDocument(data);\n  };\n\n  getRepositories = async () => {\n    const joplinClient = await this.getJoplinClient();\n    return joplinClient.getRepositories();\n  };\n\n  getTags = async () => {\n    const joplinClient = await this.getJoplinClient();\n    return joplinClient.getTags(this.config.filterTags);\n  };\n\n  private async getJoplinClient(): Promise<IJoplinClient> {\n    if (!this.client) {\n      this.client = this.getSupportToken();\n    }\n    return this.client;\n  }\n\n  private async getSupportToken() {\n    const tokens = this.config.token\n      .split('\\n')\n      .map(o => o.trim())\n      .filter(p => !!p);\n    for (let i = 0; i < tokens.length; i++) {\n      const token = tokens[i];\n      try {\n        const client = await this._getJoplinClient(token);\n        const repositories = await client.getRepositories();\n        if (Array.isArray(repositories)) {\n          console.log(`Check token ${i} success.`);\n          return client;\n        }\n        return client;\n      } catch (_error) {\n        //\n        console.log(`Check token ${i} error.`);\n      }\n    }\n\n    throw new Error('invalid Token');\n  }\n\n  private async _getJoplinClient(token: string): Promise<IJoplinClient> {\n    const request = new RequestHelper({\n      baseURL: HOST,\n      request: Container.get(IBasicRequestService),\n      params: {\n        token,\n      },\n    });\n    const client = new JoplinClient(request);\n    if (await client.support()) {\n      return client;\n    }\n    return new LegacyJoplinClient(request);\n  }\n}\n"
  },
  {
    "path": "src/common/backend/services/leanote/form.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport { LeanoteBackendServiceConfig } from '../../clients/leanote/interface';\nimport { FormattedMessage } from 'react-intl';\nimport i18n from '@/common/locales';\nimport useOriginForm from '@/hooks/useOriginForm';\n\ninterface OneNoteProps {\n  verified?: boolean;\n  info?: LeanoteBackendServiceConfig;\n}\n\nconst ExtraForm: React.FC<OneNoteProps & FormComponentProps> = props => {\n  const {\n    form: { getFieldDecorator },\n    form,\n    info,\n  } = props;\n  const { verified, handleAuthentication, formRules } = useOriginForm({\n    form,\n    initStatus: !!info,\n    originKey: 'leanote_host',\n  });\n  let initData: Partial<LeanoteBackendServiceConfig> = {};\n  if (info) {\n    initData = info;\n  }\n  let editMode = info ? true : false;\n  return (\n    <Fragment>\n      <Form.Item\n        label={\n          <FormattedMessage id=\"backend.services.confluence.form.origin\" defaultMessage=\"Origin\" />\n        }\n      >\n        {form.getFieldDecorator('leanote_host', {\n          initialValue: info?.leanote_host,\n          rules: formRules,\n        })(\n          <Input.Search\n            enterButton={\n              <FormattedMessage\n                id=\"backend.services.confluence.form.authentication\"\n                defaultMessage=\"Authentication\"\n              />\n            }\n            onSearch={handleAuthentication}\n            disabled={verified}\n          />\n        )}\n      </Form.Item>\n      <Form.Item\n        label={<FormattedMessage id=\"backend.services.leanote.form.email\" defaultMessage=\"Email\" />}\n      >\n        {getFieldDecorator('email', {\n          initialValue: initData.email,\n          rules: [\n            {\n              required: true,\n              message: i18n.format({\n                id: 'backend.services.leanote.form.email',\n                defaultMessage: 'Email is required.',\n              }),\n            },\n          ],\n        })(<Input disabled={editMode} />)}\n      </Form.Item>\n      <Form.Item\n        label={\n          <FormattedMessage id=\"backend.services.leanote.form.pwd\" defaultMessage=\"Password\" />\n        }\n      >\n        {getFieldDecorator('pwd', {\n          initialValue: initData.pwd,\n        })(<Input disabled={editMode} type=\"password\" />)}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default ExtraForm;\n"
  },
  {
    "path": "src/common/backend/services/leanote/index.ts",
    "content": "import { ServiceMeta } from '@/common/backend';\nimport localeService from '@/common/locales';\nimport Service from './service';\nimport form from './form';\n\nexport default (): ServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.leanote.name',\n      defaultMessage: 'Leanote',\n    }),\n    icon: 'leanote',\n    type: 'leanote',\n    service: Service,\n    form: form,\n    homePage: 'https://github.com/leanote/leanote',\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/leanote/service.ts",
    "content": "import { CompleteStatus } from 'common/backend/interface';\nimport { DocumentService, CreateDocumentRequest } from '../../index';\nimport { IBasicRequestService } from '@/service/common/request';\nimport { Container } from 'typedi';\nimport LeanoteClient from '../../clients/leanote/client';\nimport md5 from '@web-clipper/shared/lib/md5';\nimport { LeanoteBackendServiceConfig, LeanoteNotebook } from '../../clients/leanote/interface';\n\n/**\n *\n * Document service for self hosted leanote or leanote.com\n */\nexport default class LeanoteDocumentService implements DocumentService {\n  private client: LeanoteClient;\n  private config: LeanoteBackendServiceConfig;\n\n  /**\n   * This extension will need the user and password to connect to leanote and fetch a token\n   * You must supply the one of your leanote server.\n   */\n  constructor(config: LeanoteBackendServiceConfig) {\n    this.config = config;\n    this.client = new LeanoteClient(config, Container.get(IBasicRequestService));\n  }\n\n  /** Unique account identification */\n  getId = () => {\n    return md5(`leanote_${this.config.leanote_host}_${this.config.email}`);\n  };\n\n  getUserInfo = async () => {\n    return {\n      name: this.config.leanote_host,\n      avatar: '',\n      homePage: this.config.leanote_host,\n      description: `send to ${this.config.email} account on leanote`,\n    };\n  };\n\n  /**\n   * If not logged, login then fetch notebook as repository\n   * change：getSyncNotebooks => getNotebooks\n   *\n   * @see documentation https://github.com/leanote/leanote/wiki/leanote-api\n   */\n  getRepositories = async () => {\n    let response = await this.client.getNotebooks();\n    if ((response as any).Msg && (response as any).Msg === 'NOTLOGIN') {\n      await this.client.login();\n      response = await this.client.getNotebooks();\n    }\n    return response.map(function(leanoteNotebook: LeanoteNotebook) {\n      return {\n        id: leanoteNotebook.NotebookId,\n        name: leanoteNotebook.Title,\n        groupId: 'leanote',\n        groupName: 'leanote',\n      };\n    });\n  };\n\n  /**\n   * @TODO handle Error\n   * Use the leanote api to clip document as note in leanote\n   *\n   * @see documentation https://github.com/leanote/leanote/wiki/leanote-api\n   */\n  createDocument = async (info: CreateDocumentRequest): Promise<CompleteStatus> => {\n    const result = await this.client.createDocument(info);\n    if (result.NoteId) {\n      return {\n        href: `${this.config.leanote_host}/note/${result.NoteId}`,\n      };\n    }\n    return {\n      href: `${this.config.leanote_host}`,\n    };\n  };\n\n  /**\n   * Use the leanote api to embed image in the document as note in leanote\n   *\n   * @see documentation https://github.com/leanote/leanote/wiki/leanote-api\n   */\n  uploadBlob = async (blob: Blob): Promise<string> => {\n    return this.client.uploadBlob(blob);\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/memos/form.tsx",
    "content": "import { Form } from '@ant-design/compatible';\r\nimport '@ant-design/compatible/assets/index.less';\r\nimport { Input } from 'antd';\r\nimport { FormComponentProps } from '@ant-design/compatible/es/form';\r\nimport React, { Fragment } from 'react';\r\nimport { MemosBackendServiceConfig } from './interface';\r\nimport useOriginForm from '@/hooks/useOriginForm';\r\nimport { FormattedMessage } from 'react-intl';\r\n\r\ninterface MemosFormProps {\r\n  verified?: boolean;\r\n  info?: MemosBackendServiceConfig;\r\n}\r\n\r\nconst FormItem: React.FC<MemosFormProps & FormComponentProps> = props => {\r\n  const {\r\n    form,\r\n    form: { getFieldDecorator },\r\n    info,\r\n    verified,\r\n  } = props;\r\n\r\n  const { verified: formVerified, handleAuthentication, formRules } = useOriginForm({\r\n    form,\r\n    initStatus: !!info,\r\n  });\r\n\r\n  let initData: Partial<MemosBackendServiceConfig> = {};\r\n  if (info) {\r\n    initData = info;\r\n  }\r\n  let editMode = info ? true : false;\r\n  return (\r\n    <Fragment>\r\n      <Form.Item label=\"Host\">\r\n        {getFieldDecorator('origin', {\r\n          initialValue: initData.origin || 'https://demo.usememos.com',\r\n          rules: [\r\n            {\r\n              required: true,\r\n              message: (\r\n\t\t\t\t\t\t\t\t<FormattedMessage\r\n                id=\"backend.services.memos.form.authentication\"\r\n                defaultMessage=\"Host URL requeired!\"\r\n              />\r\n\t\t\t\t\t\t\t),\r\n\t\t\t\t\t\t\ttype: 'url',\r\n            },\r\n            ...formRules,\r\n          ],\r\n        })(\r\n          <Input.Search\r\n            enterButton={\r\n              <FormattedMessage\r\n                id=\"backend.services.memos.form.hostTest\"\r\n                defaultMessage=\"test\"\r\n              />\r\n            }\r\n            disabled={editMode || formVerified}\r\n            onSearch={handleAuthentication}\r\n          />\r\n        )}\r\n      </Form.Item>\r\n      <Form.Item label=\"AccessToken\">\r\n        {getFieldDecorator('accessToken', {\r\n          initialValue: initData.accessToken,\r\n          rules: [\r\n            {\r\n              required: true,\r\n              message: (\r\n\t\t\t\t\t\t\t\t<FormattedMessage\r\n                id=\"backend.services.memos.accessToken.message\"\r\n                defaultMessage='AccessToken is required!'\r\n              />),\r\n            },\r\n          ],\r\n        })(<Input\r\n\t\t\t\t\tdisabled={editMode || verified || !formVerified}\r\n\t\t\t\t\t/>)}\r\n      </Form.Item>\r\n    </Fragment>\r\n  );\r\n};\r\n\r\nexport default FormItem;\r\n"
  },
  {
    "path": "src/common/backend/services/memos/headerForm.tsx",
    "content": "import { Input, Tooltip, Select } from 'antd';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport locales from '@/common/locales';\nimport { VisibilityType } from './interface';\n\nconst { Option } = Select;\n\nconst HeaderForm: React.FC<FormComponentProps> = ({ form: { getFieldDecorator } }) => {\n\n  return (\n    <Fragment>\n      <Form.Item>\n        <Tooltip\n          trigger={['focus']}\n          title={locales.format({\n            id: 'backend.services.memos.headerForm.tag',\n            defaultMessage: 'Input tags (eg. tag1, tag2...)'\n          })}\n          placement=\"topLeft\"\n          overlayClassName=\"numeric-input\"\n        >\n          {getFieldDecorator('tags', {\n            rules: [\n              {\n                pattern: /^(?! )[^\\u4e00-\\u9fa5~`!@#$%^&*()_+={}\\[\\]:;\"'<>.?\\/\\\\|]*[^\\s.,;:!?\"'()]*$/,\n                message: locales.format({\n                  id: 'backend.services.memos.headerForm.tag_error',\n                }),\n              },\n            ],\n          })(\n            <Input\n              autoComplete=\"off\"\n              placeholder={locales.format({\n                id: 'backend.services.memos.headerForm.tag',\n                defaultMessage: 'Input tags (eg. tag1, tag2...)'\n              })}\n            />\n          )}\n        </Tooltip>\n      </Form.Item>\n\n      <Form.Item label={locales.format({\n              id: 'backend.services.memos.headerForm.visibility',\n              defaultMessage: 'visibility'\n\t\t\t\t\t  })}>\n        {getFieldDecorator('visibility', {\n          initialValue: VisibilityType[0].value,\n        })(\n          <Select style={{ width: '100%' }}>\n            {VisibilityType.map(option => (\n              <Option key={option.value} value={option.value}>\n                {option.label()}\n              </Option>\n            ))}\n          </Select>\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default HeaderForm;\n"
  },
  {
    "path": "src/common/backend/services/memos/index.ts",
    "content": "import { ServiceMeta } from '../interface';\r\nimport Service from './service';\r\nimport Form from './form';\r\nimport localeService from '@/common/locales';\r\nimport headerForm from './headerForm';\r\n\r\nexport default (): ServiceMeta => {\r\n  return {\r\n    name: localeService.format({\r\n      id: 'backend.services.memos.name',\r\n    }),\r\n    icon: '',\r\n    type: 'memos',\r\n    service: Service,\r\n\t\theaderForm: headerForm,\r\n    form: Form,\r\n    homePage: 'https://www.usememos.com/',\r\n  };\r\n};\r\n"
  },
  {
    "path": "src/common/backend/services/memos/interface.ts",
    "content": "import { CreateDocumentRequest } from './../interface';\r\nimport locales from '@/common/locales';\r\n\r\nexport const VisibilityType = [\r\n  { label: () => locales.format({ id: 'backend.services.memos.headerForm.VisibilityType.private', defaultMessage: 'private' }), value: 'PRIVATE' },\r\n  { label: () => locales.format({ id: 'backend.services.memos.headerForm.VisibilityType.public', defaultMessage: 'public' }), value: 'PUBLIC' },\r\n] as const;\r\n\r\nexport type VisibilityType = typeof VisibilityType[number];\r\n\r\nexport interface MemosBackendServiceConfig {\r\n  accessToken: string;\r\n  origin: string;\r\n}\r\n\r\nexport interface MemosUserResponse {\r\n  name: string;\r\n  username: string;\r\n  email: string;\r\n  avatarUrl: string;\r\n  description: string;\r\n}\r\n\r\nexport interface MemosUserInfo {\r\n  name: string;\r\n  avatar: string;\r\n  homePage: string;\r\n  description: string;\r\n}\r\n\r\nexport interface MemoCreateDocumentRequest extends CreateDocumentRequest {\r\n\tvisibility?: VisibilityType;\r\n\ttags?: string;\r\n}\r\n"
  },
  {
    "path": "src/common/backend/services/memos/service.ts",
    "content": "import { DocumentService } from '../../index';\r\nimport { extend, RequestMethod } from 'umi-request';\r\nimport { CompleteStatus } from '../interface';\r\nimport { Repository } from '@/common/backend/services/interface';\r\nimport {\r\n\tMemosBackendServiceConfig,\r\n\tMemoCreateDocumentRequest,\r\n\tMemosUserResponse,\r\n\tMemosUserInfo\r\n} from './interface';\r\n\r\n\r\nexport default class MemosDocumentService implements DocumentService {\r\n  private request: RequestMethod;\r\n  private token: string;\r\n  private origin: string;\r\n\tprivate UserInfo: MemosUserInfo | null;\r\n\r\n  constructor({ accessToken, origin }: MemosBackendServiceConfig) {\r\n    const realHost = origin || 'https://demo.usememos.com';\r\n    this.request = extend({\r\n      prefix: `${realHost}/api/`,\r\n      headers: { Authorization: `Bearer ${accessToken}` },\r\n      timeout: 5000,\r\n    });\r\n    this.request.interceptors.response.use(\r\n      async response => {\r\n        if (!response.ok) {\r\n          const json = await response.clone().json();\r\n          throw new Error(`(${response.status}) Err_id=${json.code || ''}: ${json.message || '未知错误'}`);\r\n        }\r\n        return response;\r\n      },\r\n      error => {\r\n        if (error.response) {\r\n          // 服务器返回错误\r\n          return error.response.json().then((json: any) => {\r\n            throw new Error(`(${error.response.status}) code=${json.id || ''}: ${json.message || error.message || '未知错误'}`);\r\n          });\r\n        }\r\n        // (50X)网络错误等\r\n        throw new Error(`(500): ${error.message || '网络错误'}`);\r\n      },\r\n    );\r\n    this.token = accessToken;\r\n    this.origin = realHost;\r\n    this.UserInfo = null;\r\n  }\r\n\r\n  getId = () => {\r\n\t\treturn '0';\r\n\t};\r\n\r\n  getUserInfo = async (): Promise<MemosUserInfo> => {\r\n    const response = await this.request.post<MemosUserResponse>('v1/auth/status');\r\n\r\n    const MemosUserInfo: MemosUserInfo = {\r\n      name: response.username || 'Memos User',\r\n      avatar: response.avatarUrl\r\n        ? `${this.origin}${response.avatarUrl}`\r\n        : 'https://demo.usememos.com/full-logo.webp',\r\n      homePage: this.origin,\r\n      description: response.description || 'Memos User',\r\n    };\r\n\r\n    this.UserInfo = MemosUserInfo;\r\n    return MemosUserInfo;\r\n  };\r\n\r\n  private addTag = (tags: string, content: string): string => {\r\n    const tagArray = tags.split(',').map(tag => tag.trim()).filter(tag => tag);\r\n    const formattedTags = tagArray.map(tag => `#${tag}`).join(' ');\r\n    return `${content}\\n${formattedTags}`;\r\n  };\r\n\r\n  createDocument = async (\r\n    info: MemoCreateDocumentRequest\r\n  ): Promise<CompleteStatus> => {\r\n    if (!this.UserInfo) {\r\n      this.UserInfo = await this.getUserInfo();\r\n    }\r\n\r\n    if (info.tags) {\r\n      info.content = this.addTag(info.tags, info.content);\r\n    }\r\n\r\n    const response = await this.request.post<{\r\n      id: string;\r\n      content: string;\r\n      creator: string;\r\n    }>('v1/memos', {\r\n      data: {\r\n        content: info.content,\r\n        visibility: info.visibility || 'PRIVATE',\r\n      },\r\n    });\r\n\r\n    return {\r\n      href: `${this.origin}/u/${this.UserInfo.name}`,\r\n    };\r\n  };\r\n\r\n  getRepositories = async (): Promise<Repository[]> => {\r\n    return [{\r\n      id: 'memos_default',\r\n      name: '默认分区 Default Repo',\r\n      groupId: 'memos',\r\n      groupName: '默认分组 Defualt Group',\r\n    }];\r\n  };\r\n}\r\n"
  },
  {
    "path": "src/common/backend/services/notion/index.ts",
    "content": "import { ServiceMeta } from '@/common/backend';\nimport Service from './service';\n\nexport default (): ServiceMeta => {\n  return {\n    name: 'Notion',\n    icon: 'https://www.notion.so/images/favicon.ico',\n    type: 'notion',\n    homePage: 'https://www.notion.so/',\n    service: Service,\n    permission: {\n      origins: ['https://www.notion.so/*'],\n      permissions: ['cookies'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/notion/service.ts",
    "content": "import localeService from '@/common/locales';\nimport { ICookieService } from '@/service/common/cookie';\nimport { IWebRequestService } from '@/service/common/webRequest';\nimport { generateUuid } from '@web-clipper/shared/lib/uuid';\nimport axios, { AxiosInstance } from 'axios';\nimport Container from 'typedi';\nimport { CreateDocumentRequest, DocumentService } from '../../index';\nimport { CompleteStatus, UnauthorizedError } from './../interface';\nimport { NotionRepository, NotionUserContent, RecentPages } from './types';\n\nconst PAGE = 'page';\nconst COLLECTION_VIEW_PAGE = 'collection_view_page';\nconst origin = 'https://www.notion.so/';\n\nexport default class NotionDocumentService implements DocumentService {\n  private request: AxiosInstance;\n  private repositories: NotionRepository[];\n  private userContent?: NotionUserContent;\n  private webRequestService: IWebRequestService;\n  private cookieService: ICookieService;\n\n  constructor() {\n    const request = axios.create({\n      baseURL: origin,\n      timeout: 10000,\n      transformResponse: [\n        (data): any => {\n          return JSON.parse(data);\n        },\n      ],\n      withCredentials: true,\n    });\n    this.request = request;\n    this.repositories = [];\n    this.webRequestService = Container.get(IWebRequestService);\n    this.cookieService = Container.get(ICookieService);\n    this.request.interceptors.response.use(\n      (r) => r,\n      (error) => {\n        if (error.response && error.response.status === 401) {\n          return Promise.reject(\n            new UnauthorizedError(\n              localeService.format({\n                id: 'backend.services.notion.unauthorizedErrorMessage',\n                defaultMessage: 'Unauthorized! Please Login Notion Web.',\n              })\n            )\n          );\n        }\n        return Promise.reject(error);\n      }\n    );\n  }\n\n  getId = () => {\n    return 'notion';\n  };\n\n  getUserInfo = async () => {\n    if (!this.userContent) {\n      this.userContent = await this.getUserContent();\n    }\n    const user = this.userContent.recordMap.notion_user;\n    const userInfo = Object.values(user)[0];\n    const { email, profile_photo, name } = userInfo.value;\n    return {\n      name,\n      avatar: profile_photo,\n      homePage: 'https://www.notion.so/',\n      description: email,\n    };\n  };\n\n  getRepositories = async () => {\n    if (!this.userContent) {\n      this.userContent = await this.getUserContent();\n    }\n\n    const userId = Object.keys(this.userContent.recordMap.notion_user)[0] as string;\n    const spaces = (await this.getSpaces(userId)) as any;\n    const result: Array<NotionRepository[]> = await Promise.all(\n      Object.keys(spaces).map(async (p) => {\n        const space = spaces[p];\n        const recentPages = await this.getRecentPageVisits(space.spaceId, userId);\n        const spaceName = await this.getSpaceName(space.spaceId);\n        return this.loadSpace(space.spaceId, spaceName, recentPages);\n      })\n    );\n\n    this.repositories = result.flat() as NotionRepository[];\n    return this.repositories;\n  };\n\n  getSpaces = async (userId: string) => {\n    const response = await this.requestWithCookie.post<{\n      users: {\n        [id: string]: {\n          user_root: {\n            [id: string]: {\n              value: {\n                space_view_pointers: [\n                  {\n                    id: string;\n                    table: string;\n                    spaceId: string;\n                  }\n                ]\n              }\n            };\n          }\n          space: any;\n        };\n      };\n    }>('/api/v3/getSpacesInitial');\n    return response.data.users[userId].user_root[userId].value.space_view_pointers;\n  };\n\n  getSpaceName = async (spaceId: string) => {\n    const response = await this.requestWithCookie.post<{\n      results: [\n        {\n          name: string;\n        }\n      ]\n    }>('api/v3/getPublicSpaceData', {\n      spaceIds: [spaceId],\n      type: 'space-ids'\n    });\n    return response.data.results[0].name;\n  }\n\n  createDocument = async ({\n    repositoryId,\n    title,\n    content,\n  }: CreateDocumentRequest): Promise<CompleteStatus> => {\n    let fileName = `${title}.md`;\n\n    const repository = this.repositories.find((o) => o.id === repositoryId);\n    if (!repository) {\n      throw new Error('Illegal repository');\n    }\n\n    const documentId = await this.createEmptyFile(repository, content);\n    const fileUrl = await this.getFileUrl(encodeURI(fileName));\n    await axios.put(fileUrl.signedPutUrl, `${content}`, {\n      headers: {\n        'Content-Type': 'text/markdown',\n      },\n    });\n    if (!this.userContent) {\n      this.userContent = await this.getUserContent();\n    }\n    const spaceId = await this.getSpaceId();\n    await this.requestWithCookie.post('api/v3/enqueueTask', {\n      task: {\n        eventName: 'importFile',\n        request: {\n          fileURL: fileUrl.url,\n          fileName,\n          importType: 'ReplaceBlock',\n          block: {\n            id: documentId,\n            spaceId: spaceId,\n          },\n          spaceId: spaceId,\n          signedToken: fileUrl.signedToken,\n        },\n      },\n    });\n\n    return {\n      href: `https://www.notion.so/${repository.groupId}/${documentId.replace(/-/g, '')}`,\n    };\n  };\n\n  getSpaceId = async () => {\n    if (!this.userContent) {\n      this.userContent = await this.getUserContent();\n    }\n\n    const userId = Object.keys(this.userContent.recordMap.notion_user)[0] as string;\n    const spaces = (await this.getSpaces(userId)) as any;\n    return spaces[0].spaceId;\n  };\n\n  createEmptyFile = async (repository: NotionRepository, title: string) => {\n    if (!this.userContent) {\n      this.userContent = await this.getUserContent();\n    }\n    const spaceId = await this.getSpaceId();\n    const documentId = generateUuid();\n    const requestId = generateUuid();\n    const inner_requestId = generateUuid();\n    const parentId = repository.id;\n    const userId = Object.values(this.userContent.recordMap.notion_user)[0].value.id;\n    const time = new Date().getDate();\n    let operations;\n    if (repository.pageType === PAGE) {\n      operations = [\n        {\n          id: documentId,\n          table: 'block',\n          path: [],\n          command: 'set',\n          args: {\n            type: 'page',\n            id: documentId,\n            space_id: spaceId,\n            version: 1,\n          },\n        },\n        {\n          id: documentId,\n          table: 'block',\n          path: [],\n          command: 'update',\n          args: {\n            parent_id: parentId,\n            parent_table: 'block',\n            alive: true,\n            space_id: spaceId,\n          },\n        },\n        {\n          table: 'block',\n          id: parentId,\n          path: ['content'],\n          command: 'listAfter',\n          args: {\n            id: documentId,\n            space_id: spaceId,\n          },\n        },\n        {\n          id: documentId,\n          table: 'block',\n          path: [],\n          command: 'update',\n          args: {\n            created_by: userId,\n            created_time: time,\n            last_edited_time: time,\n            last_edited_by: userId,\n            space_id: spaceId,\n          },\n        },\n        {\n          id: parentId,\n          table: 'block',\n          path: [],\n          command: 'update',\n          args: {\n            last_edited_time: time,\n            space_id: spaceId,\n          },\n        },\n        {\n          id: documentId,\n          table: 'block',\n          path: ['properties', 'title'],\n          command: 'set',\n          args: [[title]],\n        },\n        {\n          id: documentId,\n          table: 'block',\n          path: [],\n          command: 'update',\n          args: {\n            last_edited_time: time,\n            space_id: spaceId,\n          },\n        },\n      ];\n    } else if (repository.pageType === COLLECTION_VIEW_PAGE) {\n      operations = [\n        {\n          id: documentId,\n          table: 'block',\n          path: [],\n          command: 'set',\n          args: {\n            type: 'page',\n            id: documentId,\n            space_id: spaceId,\n            version: 1,\n          },\n        },\n        {\n          id: documentId,\n          table: 'block',\n          path: [],\n          command: 'update',\n          args: {\n            parent_id: parentId,\n            parent_table: 'collection',\n            space_id: spaceId,\n            alive: true,\n          },\n        },\n      ];\n    }\n\n    await this.requestWithCookie.post('api/v3/saveTransactionsFanout', {\n      requestId: requestId,\n      transactions: [\n        {\n          id: inner_requestId,\n          operations: operations,\n          spaceId: spaceId,\n        }\n      ]\n    });\n    return documentId;\n  };\n\n  getFileUrl = async (fileName: string) => {\n    const result = await this.requestWithCookie.post<{\n      url: string;\n      signedPutUrl: string;\n      signedToken: string;\n    }>('api/v3/getUploadFileUrl', {\n      bucket: 'temporary',\n      name: fileName,\n      contentType: 'text/markdown',\n    });\n    return result.data;\n  };\n\n  private async loadSpace(\n    spaceId: string,\n    spaceName: string,\n    recentPages: RecentPages\n  ): Promise<NotionRepository[]> {\n    const response = await this.requestWithCookie.post<{\n      pages: string[];\n      recordMap: {\n        block: {\n          [id: string]: {\n            value: {\n              collection_id: string;\n              id: string;\n              type: string;\n              space_id: string;\n              properties: {\n                title: string[];\n              };\n            };\n          };\n        };\n      };\n    }>('api/v3/getUserSharedPagesInSpace', {\n      includeDeleted: false,\n      includeTeamSharedPages: false,\n      spaceId,\n    });\n\n    const pages: string[] = response.data.pages as string[];\n\n    return pages\n      .map((pageId): NotionRepository | null => {\n        const value = response.data.recordMap.block[pageId]!.value;\n        if (value.type === PAGE && !!value.properties && !!value.properties.title) {\n          return {\n            id: value.id,\n            name: value.properties.title.toString(),\n            groupId: spaceId,\n            groupName: spaceName,\n            pageType: PAGE,\n          };\n        }\n        const collections = recentPages.recordMap.collection;\n        if (\n          value.type === COLLECTION_VIEW_PAGE &&\n          !!value.collection_id &&\n          !!collections &&\n          !!collections[value.collection_id] &&\n          !!collections[value.collection_id].value &&\n          !!collections[value.collection_id].value.name\n        ) {\n          return {\n            id: collections[value.collection_id].value.id,\n            name: collections[value.collection_id].value.name.toString(),\n            groupId: spaceId,\n            groupName: spaceName,\n            pageType: COLLECTION_VIEW_PAGE,\n          };\n        }\n        return null;\n      })\n      .filter((p): p is NotionRepository => !!p);\n  }\n\n  private async getRecentPageVisits(spaceId: string, userId: string): Promise<RecentPages> {\n    const res = await this.requestWithCookie.post<RecentPages>('api/v3/getRecentPageVisits', {\n      spaceId,\n      userId,\n    });\n    return res.data;\n  }\n\n  private getUserContent = async () => {\n    const response = await this.requestWithCookie.post<NotionUserContent>('api/v3/loadUserContent');\n    return response.data;\n  };\n\n  /**\n   * Modify the cookie when request\n   */\n  private get requestWithCookie() {\n    const post = async <T>(url: string, data?: any) => {\n      const cookies = await this.cookieService.getAll({\n        url: origin,\n      });\n      const cookieString = cookies.map((o) => `${o.name}=${o.value}`).join(';');\n      const header = await this.webRequestService.startChangeHeader({\n        urls: [`${origin}*`],\n        requestHeaders: [\n          {\n            name: 'cookie',\n            value: cookieString,\n          },\n          {\n            name: `Content-Type`,\n            value: 'application/json',\n          },\n        ],\n      });\n      try {\n        const result = await this.request.post<T>(\n          await this.webRequestService.changeUrl(url, header),\n          data,\n          {}\n        );\n        await this.webRequestService.end(header);\n        return result;\n      } catch (error) {\n        await this.webRequestService.end(header);\n        throw error;\n      }\n    };\n    return {\n      post,\n    };\n  }\n}\n"
  },
  {
    "path": "src/common/backend/services/notion/types.ts",
    "content": "import { Repository } from '../interface';\n\nexport interface NotionUserContent {\n  recordMap: {\n    notion_user: {\n      [uuid: string]: {\n        role: string;\n        value: {\n          name: string;\n          id: string;\n          email: string;\n          profile_photo: string;\n        };\n      };\n    };\n    space: {\n      [id: string]: {\n        role: string;\n        value: {\n          id: string;\n          name: string;\n          domain: string;\n          pages: string[];\n        };\n      };\n    };\n    block: {\n      [uuid: string]: {\n        role: string;\n        value: {\n          id: string;\n          version: string;\n          parent_id: string;\n          type: string;\n          created_time: number;\n          properties: {\n            title: string[][];\n            content: string[];\n          };\n          collection_id: string;\n        };\n      };\n    };\n    collection: {\n      [uuid: string]: {\n        role: string;\n        value: {\n          id: string;\n          version: string;\n          parent_id: string;\n          name: string[][];\n        };\n      };\n    };\n  };\n}\n\nexport interface RecentPages {\n  recordMap: {\n    collection?: {\n      [uuid: string]: {\n        role: string;\n        value: {\n          id: string;\n          version: string;\n          parent_id: string;\n          name: string[][];\n        };\n      };\n    };\n  };\n}\n\nexport interface NotionRepository extends Repository {\n  pageType: string;\n}\n"
  },
  {
    "path": "src/common/backend/services/obsidian/form.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport { Input } from 'antd';\nimport React, { Component, Fragment } from 'react';\nimport { ObsidianFormConfig } from './interface';\n\ninterface OneNoteProps {\n  info?: ObsidianFormConfig;\n}\n\nexport default class extends Component<OneNoteProps & FormComponentProps> {\n  render() {\n    const {\n      form: { getFieldDecorator },\n      info,\n    } = this.props;\n    let initData: Partial<ObsidianFormConfig> = {};\n    if (info) {\n      initData = info;\n    }\n    return (\n      <Fragment>\n        <Form.Item label=\"Vault\">\n          {getFieldDecorator('vault', {\n            initialValue: initData.vault,\n            rules: [\n              {\n                required: true,\n                message: 'Please input your vault!',\n              },\n            ],\n          })(<Input />)}\n        </Form.Item>\n        <Form.Item label=\"Save Folder\">\n          {getFieldDecorator('folder', {\n            initialValue: initData.folder,\n            rules: [\n              {\n                required: true,\n                message: 'Please input the folders you want to save!',\n              },\n            ],\n          })(<Input.TextArea placeholder='Please enter the folders you want to save, one per line.' />)}\n        </Form.Item>\n      </Fragment>\n    );\n  }\n}\n"
  },
  {
    "path": "src/common/backend/services/obsidian/index.ts",
    "content": "import localeService from '@/common/locales';\nimport { ServiceMeta } from './../interface';\nimport Service from './service';\nimport From from './form';\n\nexport default (): ServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.obsidian.name',\n      defaultMessage: 'Obsidian',\n    }),\n    form: From,\n    icon: 'obsidian',\n    type: 'obsidian',\n    service: Service,\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/obsidian/interface.ts",
    "content": "export interface ObsidianFormConfig {\n  vault: string;\n  folder: string;\n}\n"
  },
  {
    "path": "src/common/backend/services/obsidian/service.ts",
    "content": "import md5 from '@web-clipper/shared/lib/md5';\nimport { CreateDocumentRequest, DocumentService } from '../../index';\nimport { ObsidianFormConfig } from './interface';\nimport QueryString from 'query-string';\n\nexport default class ObsidianService implements DocumentService {\n  constructor(private config: ObsidianFormConfig) {}\n  getId = () => {\n    return md5(JSON.stringify(this.config));\n  };\n\n  getUserInfo = async () => {\n    return {\n      name: 'Obsidian',\n      avatar: '',\n      description: `Vault: ${this.config.vault}`,\n    };\n  };\n\n  getRepositories = async () => {\n    const folders = (this.config.folder || '').split('\\n').map((folder) => {\n      return {\n        id: folder,\n        name: folder,\n        groupId: 'obsidian',\n        groupName: this.config.vault,\n      };\n    });\n    return folders;\n  };\n\n  createDocument = async (info: CreateDocumentRequest) => {\n    const file = `${info.repositoryId}/${info.title}`;\n    window.open(\n      QueryString.stringifyUrl({\n        url: 'obsidian://new',\n        query: {\n          silent: true,\n          vault: this.config.vault,\n          file,\n          content: info.content,\n        },\n      })\n    );\n    return {\n      href: QueryString.stringifyUrl({\n        url: 'obsidian://open',\n        query: {\n          vault: this.config.vault,\n          file,\n        },\n      }),\n    };\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/onenote_oauth/form.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Component, Fragment } from 'react';\nimport { OneNoteBackendServiceConfig } from './interface';\n\ninterface OneNoteProps {\n  verified?: boolean;\n  info?: OneNoteBackendServiceConfig;\n}\n\nexport default class extends Component<OneNoteProps & FormComponentProps> {\n  render() {\n    const {\n      form: { getFieldDecorator },\n      info,\n      verified,\n    } = this.props;\n\n    let initData: Partial<OneNoteBackendServiceConfig> = {};\n    if (info) {\n      initData = info;\n    }\n    let editMode = info ? true : false;\n    return (\n      <Fragment>\n        <Form.Item label=\"AccessToken\">\n          {getFieldDecorator('access_token', {\n            initialValue: initData.access_token,\n            rules: [\n              {\n                required: true,\n                message: 'AccessToken is required!',\n              },\n            ],\n          })(<Input disabled={editMode || verified} />)}\n        </Form.Item>\n        <Form.Item label=\"RefreshToken\">\n          {getFieldDecorator('refresh_token', {\n            initialValue: initData.refresh_token,\n            rules: [\n              {\n                required: true,\n                message: 'RefreshToken is required!',\n              },\n            ],\n          })(<Input disabled={editMode || verified} />)}\n        </Form.Item>\n      </Fragment>\n    );\n  }\n}\n"
  },
  {
    "path": "src/common/backend/services/onenote_oauth/index.ts",
    "content": "import { IConfigService } from '@/service/common/config';\nimport { Container } from 'typedi';\nimport config from '@/config';\nimport { ServiceMeta } from './../interface';\nimport Service from './service';\nimport localeService from '@/common/locales';\nimport { stringify } from 'qs';\nimport form from './form';\n\nexport default (): ServiceMeta => {\n  const oauthUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${stringify({\n    scope: 'Notes.Create User.Read offline_access',\n    client_id: config.oneNoteClientId,\n    state: Container.get(IConfigService).id,\n    response_type: 'code',\n    response_mode: 'query',\n    redirect_uri: config.oneNoteCallBack,\n  })}`;\n\n  return {\n    name: localeService.format({\n      id: 'backend.services.onenote_oauth.name',\n      defaultMessage: 'OneNote',\n    }),\n    icon: 'OneNote',\n    type: 'onenote_oauth',\n    service: Service,\n    oauthUrl,\n    form: form,\n    homePage: 'https://products.office.com/en-us/onenote/digital-note-taking-app',\n    permission: {\n      origins: ['https://graph.microsoft.com/*', 'https://login.microsoftonline.com/*'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/onenote_oauth/interface.ts",
    "content": "export interface OneNoteBackendServiceConfig {\n  refresh_token: string;\n  access_token: string;\n}\n\nexport interface OneNoteNotebooksResponse {\n  value: {\n    id: string;\n    displayName: string;\n    sections: {\n      id: string;\n      displayName: string;\n    }[];\n  }[];\n}\n\nexport interface OneNoteUserInfoResponse {\n  id: string;\n  displayName: string;\n  userPrincipalName: string;\n}\n\nexport interface OneNoteCreateDocumentResponse {\n  id: string;\n  links: {\n    oneNoteClientUrl: {\n      href: string;\n    };\n    oneNoteWebUrl: {\n      href: string;\n    };\n  };\n}\n\nexport interface OneNoteRefreshTokenResponse {\n  access_token: string;\n  refresh_token: string;\n}\n"
  },
  {
    "path": "src/common/backend/services/onenote_oauth/service.ts",
    "content": "import { DocumentService, CreateDocumentRequest } from './../../index';\nimport axios, { AxiosInstance } from 'axios';\nimport md5 from '@web-clipper/shared/lib/md5';\nimport {\n  OneNoteNotebooksResponse,\n  OneNoteUserInfoResponse,\n  OneNoteCreateDocumentResponse,\n  OneNoteBackendServiceConfig,\n  OneNoteRefreshTokenResponse,\n} from './interface';\nimport _ from 'lodash';\nimport { Repository, UserInfo, UnauthorizedError } from '../interface';\nimport showdown from 'showdown';\nimport config from '@/config';\nimport { stringify } from 'qs';\n\nconst converter = new showdown.Converter();\n\nconst BASE_URL = `https://graph.microsoft.com/`;\n\nexport default class YuqueDocumentService implements DocumentService<OneNoteBackendServiceConfig> {\n  private request: AxiosInstance;\n  private config: OneNoteBackendServiceConfig;\n  private repositories: Repository[];\n\n  constructor({ access_token, refresh_token }: OneNoteBackendServiceConfig) {\n    this.config = { access_token, refresh_token };\n    this.request = axios.create({\n      baseURL: BASE_URL,\n      headers: { Authorization: `bearer ${access_token}` },\n      timeout: 100000,\n      transformResponse: [data => JSON.parse(data)],\n      withCredentials: true,\n    });\n    this.request.interceptors.response.use(\n      r => r,\n      error => {\n        if (error.response && error.response.status === 401) {\n          const ere = new UnauthorizedError();\n          return Promise.reject(ere);\n        }\n        return Promise.reject(error);\n      }\n    );\n    this.repositories = [];\n  }\n\n  getId = () => md5(this.config.access_token);\n\n  getUserInfo = async (): Promise<UserInfo> => {\n    const response = await this.request.get<OneNoteUserInfoResponse>('v1.0/me');\n    const { data } = response;\n    return {\n      name: data.displayName,\n      description: data.userPrincipalName,\n      avatar: '',\n      homePage: 'https://www.onenote.com/notebooks',\n    };\n  };\n\n  getRepositories = async (): Promise<Repository[]> => {\n    const response = await this.request.get<OneNoteNotebooksResponse>(\n      '/v1.0/me/onenote/notebooks??expand=sections,sectionGroups'\n    );\n\n    let promises = await response.data.value.map(({ id: groupId, displayName: groupName }) => {\n      return this.getSections(true, groupId, '', groupId, groupName);\n    });\n\n    for (const p of promises) {\n      this.repositories.push(..._.flatten(await p));\n    }\n\n    return this.repositories;\n  };\n\n  /**\n   * Add sections recursively (even for those in Section Groups)\n   *\n   * @param {boolean} notebook In Microsoft API, the topmost level should be \"notebooks\",\n   *                           then \"sectionGroups\" in later levels\n   * @param {string} preId The id passing in.\n   * @param {string} prefix to denote names of Section Groups.\n   * @param {sring} groupId for notebook id.\n   * @param {string} groupName for notebook name.\n   */\n  getSections = async (\n    notebook: boolean,\n    preId: string,\n    prefix: string,\n    groupId: string,\n    groupName: string\n  ): Promise<Repository[]> => {\n    let repos = [];\n\n    // Handle sections\n    const sectionsRes = await this.request.get<OneNoteNotebooksResponse>(\n      `/v1.0/users/me/onenote/${notebook ? 'notebooks/' : 'sectionGroups/'}${preId}/sections`\n    );\n    repos.push(\n      ..._.flatten(\n        sectionsRes.data.value.map(({ id, displayName: tempName }) => {\n          let name = (prefix === '' ? '' : `${prefix}-`) + tempName;\n          return {\n            name,\n            id,\n            groupId,\n            groupName,\n          };\n        })\n      )\n    );\n\n    // Handle sectionGroups recursively\n    const sectionGroupsRes = await this.request.get<OneNoteNotebooksResponse>(\n      `/v1.0/users/me/onenote/${notebook ? 'notebooks/' : 'sectionGroups/'}${preId}/sectionGroups`\n    );\n    let promises = await sectionGroupsRes.data.value.map(({ id, displayName: subPrefix }) => {\n      let newPrefix = (prefix === '' ? '' : `${prefix}-`) + subPrefix;\n      return this.getSections(false, id, newPrefix, groupId, groupName);\n    });\n\n    for (const p of promises) {\n      repos.push(..._.flatten(await p));\n    }\n\n    return repos;\n  };\n\n  createDocument = async (info: CreateDocumentRequest): Promise<any> => {\n    const { repositoryId } = info;\n    const repository = this.repositories.find(o => o.id === repositoryId);\n    if (!repository) {\n      throw new Error('Illegal repositoryId');\n    }\n    let formData = new FormData();\n    const html = `<!DOCTYPE html>\n    <html>\n      <head>\n        <title>${info.title}</title>\n      </head>\n      <body>\n       ${converter.makeHtml(`${info.content}`)}\n      </body>\n    </html>`;\n    const blob = new Blob([html], {\n      type: 'text/html',\n    });\n    formData.append('Presentation', blob);\n    const result = await this.request.post<OneNoteCreateDocumentResponse>(\n      `v1.0/me/onenote/sections/${encodeURI(repositoryId)}/pages`,\n      formData\n    );\n    return {\n      href: result.data.links.oneNoteWebUrl.href,\n    };\n  };\n\n  refreshToken = async ({ access_token, refresh_token, ...rest }: OneNoteBackendServiceConfig) => {\n    const response = await this.request.post<OneNoteRefreshTokenResponse>(\n      'https://login.microsoftonline.com/common/oauth2/v2.0/token',\n      stringify({\n        scope: 'Notes.Create User.Read offline_access',\n        redirect_uri: config.oneNoteCallBack,\n        grant_type: 'refresh_token',\n        client_id: config.oneNoteClientId,\n        refresh_token,\n      })\n    );\n    return {\n      ...rest,\n      access_token: response.data.access_token,\n      refresh_token: response.data.refresh_token,\n    };\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/server_chan/form.tsx",
    "content": "import { KeyOutlined } from '@ant-design/icons';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport { FormattedMessage } from 'react-intl';\n\ninterface FormProps extends FormComponentProps {\n  verified?: boolean;\n  info?: {\n    accessToken: string;\n  };\n}\n\nconst ConfigForm: React.FC<FormProps> = ({ form: { getFieldDecorator }, info, verified }) => {\n  const disabled = verified || !!info;\n  let initAccessToken;\n  if (info) {\n    initAccessToken = info.accessToken;\n  }\n  return (\n    <Fragment>\n      <Form.Item label=\"AccessToken\">\n        {getFieldDecorator('accessToken', {\n          initialValue: initAccessToken,\n          rules: [\n            {\n              required: true,\n              message: (\n                <FormattedMessage\n                  id=\"backend.services.server_chan.accessToken.message\"\n                  defaultMessage=\"AccessToken is required\"\n                />\n              ),\n            },\n          ],\n        })(\n          <Input\n            disabled={disabled}\n            suffix={\n              <a href={'https://sc.ftqq.com/'} target={'https://sc.ftqq.com/'}>\n                <KeyOutlined />\n              </a>\n            }\n          />\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default ConfigForm;\n"
  },
  {
    "path": "src/common/backend/services/server_chan/index.ts",
    "content": "import Service from './service';\nimport Form from './form';\nimport localeService from '@/common/locales';\n\nexport default () => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.server_chan.name',\n    }),\n    icon: 'wechat',\n    type: 'server_chan',\n    service: Service,\n    form: Form,\n    homePage: 'https://sc.ftqq.com/',\n    permission: {\n      origins: ['https://sc.ftqq.com/*'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/server_chan/service.ts",
    "content": "import localeService from '@/common/locales';\nimport { CompleteStatus } from 'common/backend/interface';\nimport { DocumentService, CreateDocumentRequest } from '../../index';\nimport request from 'umi-request';\n\nexport default class GithubDocumentService implements DocumentService {\n  private config: { accessToken: string };\n\n  constructor(config: { accessToken: string }) {\n    this.config = config;\n  }\n\n  getId = () => {\n    return this.config.accessToken;\n  };\n\n  getUserInfo = async () => {\n    return {\n      name: localeService.format({\n        id: 'backend.services.server_chan.name',\n      }),\n      avatar: '',\n      homePage: 'https://sc.ftqq.com/',\n      description: localeService.format({\n        id: 'backend.services.server_chan.name',\n      }),\n    };\n  };\n\n  getRepositories = async () => {\n    return [\n      {\n        id: 'server_chan',\n        name: localeService.format({\n          id: 'backend.services.server_chan.name',\n        }),\n        groupId: 'server_chan',\n        groupName: localeService.format({\n          id: 'backend.services.server_chan.name',\n        }),\n      },\n    ];\n  };\n\n  createDocument = async (info: CreateDocumentRequest): Promise<CompleteStatus> => {\n    const res = await request.post<{ errmsg: string; errno: number }>(\n      `https://sc.ftqq.com/${this.config.accessToken}.send`,\n      {\n        requestType: 'form',\n        data: {\n          text: info.title,\n          desp: info.content,\n        },\n      }\n    );\n    if (res.errno !== 0) {\n      throw new Error(res.errmsg);\n    }\n    return {\n      href: `http://sc.ftqq.com/`,\n    };\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/siyuan/form.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport localeService from '@/common/locales';\n\ninterface SiyuanFormProps {\n  info?: SiyuanBackendServiceConfig;\n}\n\ninterface SiyuanBackendServiceConfig {\n  accessToken?: string;\n}\n\nconst form: React.FC<FormComponentProps & SiyuanFormProps> = props => {\n  const {\n    form: { getFieldDecorator },\n    info,\n  } = props;\n\n  let initData: Partial<SiyuanBackendServiceConfig> = {};\n  if (info) {\n    initData = info;\n  }\n  let editMode = info ? true : false;\n  return (\n    <Fragment>\n      <Form.Item\n        label={localeService.format({\n          id: 'backend.services.siyuan.form.accessToken',\n        })}\n      >\n        {getFieldDecorator('accessToken', {\n          initialValue: initData.accessToken,\n        })(<Input disabled={editMode} />)}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default form;\n"
  },
  {
    "path": "src/common/backend/services/siyuan/index.ts",
    "content": "import localeService from '@/common/locales';\r\nimport { ServiceMeta } from './../interface';\r\nimport Service from './service';\r\nimport form from './form';\r\n\r\n/**\r\n * @see https://github.com/siyuan-note/siyuan/issues/1266\r\n */\r\nexport default () => {\r\n  return {\r\n    name: localeService.format({\r\n      id: 'backend.services.siyuan.name',\r\n    }),\r\n    icon: 'siyuan',\r\n    form,\r\n    type: 'siyuan',\r\n    service: Service,\r\n    homePage: 'https://b3log.org/siyuan/',\r\n    permission: {\r\n      origins: ['http://localhost:6806/*', 'http://127.0.0.1:6806/*'],\r\n    },\r\n  } as ServiceMeta;\r\n};\r\n"
  },
  {
    "path": "src/common/backend/services/siyuan/service.ts",
    "content": "import { CompleteStatus } from 'common/backend/interface';\nimport { Repository, CreateDocumentRequest } from './../interface';\nimport { DocumentService } from '../../index';\nimport { IBasicRequestService } from '@/service/common/request';\nimport { Container } from 'typedi';\nimport { SiYuanClient } from '../../clients/siyuan/client';\nimport localeService from '@/common/locales';\n\n/**\n *\n * Document service for self hosted leanote or leanote.com\n */\nexport default class SiYuanDocumentService implements DocumentService {\n  private client: SiYuanClient;\n  constructor(config: { accessToken?: string }) {\n    this.client = new SiYuanClient({\n      request: Container.get(IBasicRequestService),\n      accessToken: config.accessToken,\n    });\n  }\n\n  /** Unique account identification */\n  getId = () => {\n    return 'siyuan';\n  };\n\n  getUserInfo = async () => {\n    return {\n      name: 'siyuan',\n      avatar: '',\n      homePage: '',\n      description: ``,\n    };\n  };\n\n  getRepositories = async () => {\n    let response = await this.client.listNotebooks();\n    return response.map(\n      ({ name, id }): Repository => {\n        return {\n          groupId: 'siyuan',\n          groupName: localeService.format({\n            id: 'backend.services.siyuan.notes',\n          }),\n          id,\n          name,\n        };\n      }\n    );\n  };\n\n  createDocument = async (data: CreateDocumentRequest): Promise<CompleteStatus | void> => {\n    const id = await this.client.createNote(data);\n    return {\n      href: `siyuan://blocks/${id}`,\n    };\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/ticktick/headerForm.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Select } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport backend from '../..';\nimport Dida365DocumentService from './service';\nimport locale from '@/common/locales';\nimport { useFetch } from '@shihengtech/hooks';\n\nconst HeaderForm: React.FC<FormComponentProps> = ({ form: { getFieldDecorator } }) => {\n  const service = backend.getDocumentService() as Dida365DocumentService;\n  const tagsResponse = useFetch(async () => service.getTags(), [service], {\n    initialState: {\n      data: [],\n    },\n  });\n\n  return (\n    <Fragment>\n      <Form.Item>\n        {getFieldDecorator('tags', {\n          initialValue: [],\n        })(\n          <Select\n            mode=\"tags\"\n            maxTagCount={3}\n            style={{ width: '100%' }}\n            placeholder={locale.format({\n              id: 'backend.services.dida365.headerForm.applyTags',\n              defaultMessage: 'Apply tags',\n            })}\n            loading={tagsResponse.loading}\n          >\n            {tagsResponse.data?.map(o => (\n              <Select.Option key={o} value={o} title={o}>\n                {o}\n              </Select.Option>\n            ))}\n          </Select>\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default HeaderForm;\n"
  },
  {
    "path": "src/common/backend/services/ticktick/index.ts",
    "content": "import localeService from '@/common/locales';\nimport { ServiceMeta } from '@/common/backend';\nimport Service from './service';\nimport headerForm from './headerForm';\n\nexport default (): ServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.ticktick.name',\n      defaultMessage: 'TickTick',\n    }),\n    icon: 'dida365',\n    type: 'ticktick',\n    headerForm,\n    service: Service,\n    permission: {\n      origins: ['https://api.ticktick.com/*'],\n      permissions: [],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/ticktick/service.ts",
    "content": "import { Container } from 'typedi';\nimport { generateUuid } from '@web-clipper/shared/lib/uuid';\nimport localeService from '@/common/locales';\nimport { IWebRequestService } from '@/service/common/webRequest';\nimport {\n  CreateDocumentRequest,\n  CompleteStatus,\n  UnauthorizedError,\n  Repository,\n} from '@/common/backend/services/interface';\nimport { DocumentService } from '@/common/backend/index';\nimport { extend, RequestMethod } from 'umi-request';\n\ninterface TickTickProfile {\n  name: string;\n  username: string;\n  picture: string;\n}\n\ninterface TickTickCheckResponse {\n  projectProfiles: {\n    id: string;\n    name: string;\n    isOwner: boolean;\n    closed: boolean;\n    groupId: string;\n  }[];\n  projectGroups: {\n    id: string;\n    name: string;\n  }[];\n  tags: {\n    name: string;\n  }[];\n}\ninterface TickTickCreateDocumentRequest extends CreateDocumentRequest {\n  tags: string[];\n}\n\nexport default class TickTickDocumentService implements DocumentService {\n  private request: RequestMethod;\n\n  constructor() {\n    const request = extend({\n      prefix: `https://api.ticktick.com/api/v2/`,\n    });\n    request.interceptors.response.use(\n      (response) => {\n        if (response.clone().status === 401) {\n          throw new UnauthorizedError(\n            localeService.format({\n              id: 'backend.services.ticktick.unauthorizedErrorMessage',\n              defaultMessage: 'Unauthorized! Please Login TickTick Web.',\n            })\n          );\n        }\n        return response;\n      },\n      { global: false }\n    );\n\n    this.request = request;\n  }\n\n  getId = () => {\n    return 'TickTick';\n  };\n\n  getUserInfo = async () => {\n    const response = await this.request.get<TickTickProfile>('user/profile');\n    return {\n      name: response.name,\n      avatar: response.picture,\n      homePage: '',\n      description: response.username,\n    };\n  };\n\n  getTags = async (): Promise<string[]> => {\n    const TickTickCheckResponse = await this.request.get<TickTickCheckResponse>(`batch/check/0`);\n    return TickTickCheckResponse.tags.map((o) => o.name);\n  };\n\n  getRepositories = async (): Promise<Repository[]> => {\n    const TickTickCheckResponse = await this.request.get<TickTickCheckResponse>(`batch/check/0`);\n    const groupMap = new Map<string, string>();\n    TickTickCheckResponse.projectGroups.forEach((group) => {\n      groupMap.set(group.id, group.name);\n    });\n    return TickTickCheckResponse.projectProfiles\n      .filter((o) => !o.closed)\n      .map(({ id, name, groupId }) => ({\n        id: id,\n        name: name,\n        groupId: groupId\n          ? groupId\n          : localeService.format({\n              id: 'backend.services.ticktick.rootGroup',\n              defaultMessage: 'Root',\n            }),\n        groupName: groupId\n          ? groupMap.get(groupId)!\n          : localeService.format({\n              id: 'backend.services.ticktick.rootGroup',\n              defaultMessage: 'Root',\n            }),\n      }));\n  };\n\n  createDocument = async (request: TickTickCreateDocumentRequest): Promise<CompleteStatus> => {\n    const webRequestService = Container.get(IWebRequestService);\n    const header = await webRequestService.startChangeHeader({\n      urls: ['https://api.ticktick.com/*'],\n      requestHeaders: [\n        {\n          name: 'origin',\n          value: 'https://ticktick.com',\n        },\n      ],\n    });\n\n    const settings = await this.request.get<{ timeZone: string }>(\n      await webRequestService.changeUrl('user/preferences/settings?includeWeb=true', header)\n    );\n\n    const id = generateUuid().replace(/-/g, '').slice(0, 24);\n    const data = {\n      add: [\n        {\n          items: [],\n          reminders: [],\n          exDate: [],\n          dueDate: null,\n          priority: 0,\n          progress: 0,\n          assignee: null,\n          kind: 'NOTE',\n          sortOrder: -4611733297427382000,\n          startDate: null,\n          isFloating: false,\n          status: 0,\n          deleted: 0,\n          tags: request.tags,\n          projectId: request.repositoryId,\n          title: request.title,\n          content: request.content,\n          timeZone: settings.timeZone,\n          id: id,\n        },\n      ],\n      update: [],\n      delete: [],\n    };\n\n    await this.request.post(await webRequestService.changeUrl('batch/task', header), {\n      data: data,\n      headers: {\n        [header.name]: header.value,\n      },\n    });\n\n    await webRequestService.end(header);\n\n    return {\n      href: `https://ticktick.com/#p/${request.repositoryId}/tasks/${id}`,\n    };\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/ulysses/form.tsx",
    "content": "import React from 'react';\nimport { FormattedMessage } from 'react-intl';\n\nexport default () => (\n  <div style={{ textAlign: 'right' }}>\n    <FormattedMessage\n      id=\"backend.services.ulysses.form.confirm\"\n      defaultMessage=\"Please confirm that the Ulysses client is installed.\"\n    />\n  </div>\n);\n"
  },
  {
    "path": "src/common/backend/services/ulysses/index.ts",
    "content": "import Service from './service';\nimport Form from './form';\n\nexport default () => {\n  return {\n    name: 'Ulysses',\n    icon: 'ulysses',\n    type: 'ulysses',\n    service: Service,\n    form: Form,\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/ulysses/service.ts",
    "content": "import { ITabService } from './../../../../service/common/tab';\nimport { CompleteStatus } from 'common/backend/interface';\nimport { DocumentService, CreateDocumentRequest } from '../../index';\nimport Container from 'typedi';\n\nexport default class GithubDocumentService implements DocumentService {\n  getId = () => {\n    return 'ulysses';\n  };\n\n  getUserInfo = async () => {\n    return {\n      name: 'Ulysses',\n      avatar: '',\n      description: 'Ulysses app',\n    };\n  };\n\n  getRepositories = async () => {\n    return [\n      {\n        id: 'ulysses',\n        name: 'Ulysses',\n        groupId: 'ulysses',\n        groupName: 'Ulysses',\n      },\n    ];\n  };\n\n  createDocument = async (info: CreateDocumentRequest): Promise<CompleteStatus> => {\n    const text = `# ${info.title}\\n\\n${info.content}`;\n    const url = `ulysses://x-callback-url/new-sheet?text=${encodeURIComponent(text)}`;\n    Container.get(ITabService).create({ url });\n    return {\n      href: `ulysses://x-callback-url/open-recent`,\n    };\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/webdav/form.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport { WebDAVServiceConfig } from './interface';\nimport useOriginForm from '@/hooks/useOriginForm';\nimport { FormattedMessage } from 'react-intl';\ninterface FormProps extends FormComponentProps {\n  info?: WebDAVServiceConfig;\n}\n\nconst ConfigForm: React.FC<FormProps> = ({ form, form: { getFieldDecorator }, info }) => {\n  const { verified, handleAuthentication, formRules } = useOriginForm({ form, initStatus: !!info });\n  return (\n    <Fragment>\n      <Form.Item\n        label={\n          <FormattedMessage id=\"backend.services.confluence.form.origin\" defaultMessage=\"Origin\" />\n        }\n      >\n        {form.getFieldDecorator('origin', {\n          initialValue: info?.origin,\n          rules: formRules,\n        })(\n          <Input.Search\n            enterButton={\n              <FormattedMessage\n                id=\"backend.services.confluence.form.authentication\"\n                defaultMessage=\"Authentication\"\n              />\n            }\n            onSearch={handleAuthentication}\n            disabled={verified}\n          />\n        )}\n      </Form.Item>\n      {verified && (\n        <React.Fragment>\n          <Form.Item label=\"Username\">\n            {getFieldDecorator('username', {\n              initialValue: info?.username,\n              rules: [\n                {\n                  required: true,\n                },\n              ],\n            })(<Input disabled={!!info} />)}\n          </Form.Item>\n          <Form.Item label=\"Password\">\n            {getFieldDecorator('password', {\n              initialValue: info?.password,\n              rules: [\n                {\n                  required: true,\n                },\n              ],\n            })(<Input disabled={!!info} />)}\n          </Form.Item>\n        </React.Fragment>\n      )}\n    </Fragment>\n  );\n};\n\nexport default ConfigForm;\n"
  },
  {
    "path": "src/common/backend/services/webdav/index.ts",
    "content": "import Service from './service';\nimport Form from './form';\n\nexport default () => {\n  return {\n    name: 'WebDAV',\n    icon: 'webdav',\n    type: 'webdav',\n    service: Service,\n    form: Form,\n    homePage: '',\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/webdav/interface.ts",
    "content": "export interface WebDAVServiceConfig {\n  origin: string;\n  username: string;\n  password: string;\n}\n"
  },
  {
    "path": "src/common/backend/services/webdav/service.ts",
    "content": "import { WebDAVServiceConfig } from './interface';\nimport { DocumentService, CreateDocumentRequest, Repository } from './../interface';\n//@ts-ignore\n//@TODO use webdav/web\nimport { FileStat, WebDAVClient, createClient } from 'webdav/dist/web';\n\nexport default class WebDAVDocumentService implements DocumentService {\n  private auth: string;\n  private client: WebDAVClient;\n\n  constructor(private config: WebDAVServiceConfig) {\n    this.auth = btoa(`${this.config.username}:${this.config.password}`);\n    const originData: {\n      [key: string]: string;\n    } = {\n      'https://dav.jianguoyun.com': 'https://dav.jianguoyun.com/dav',\n    };\n    const entryPoint = originData[this.config.origin] ?? this.config.origin;\n    this.client = createClient(entryPoint, {\n      username: this.config.username,\n      password: this.config.password,\n    });\n  }\n\n  getId = () => {\n    return this.auth;\n  };\n\n  getUserInfo = async () => {\n    return {\n      name: 'WebDAV',\n      avatar: '',\n      description: this.config.username,\n    };\n  };\n\n  getRepositories = async (): Promise<Repository[]> => {\n    const result = await this.getChildrenList();\n    return result.map(o => ({\n      id: o.href,\n      name: o.displayname,\n      groupId: 'Root',\n      groupName: 'Root',\n    }));\n  };\n\n  getChildrenList = async (parent = '/'): Promise<{ displayname: string; href: string }[]> => {\n    const list = await this.client.getDirectoryContents(parent).then(files =>\n      (files as FileStat[]).map(file => ({\n        href: file.filename,\n        displayname: file.basename,\n      }))\n    );\n    console.log({ list });\n\n    return list;\n  };\n\n  createDocument = async (info: CreateDocumentRequest): Promise<void> => {\n    await this.client.putFileContents(\n      `${info.repositoryId}/${info.title.replace(/[\\\\/]/g, '_')}.md`,\n      info.content\n    );\n    return;\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/wiznote/form.tsx",
    "content": "import React from 'react';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport { WizNoteConfig } from '@/common/backend/services/wiznote/interface';\nimport { FormattedMessage } from 'react-intl';\nimport useOriginForm from '@/hooks/useOriginForm';\n\ninterface WizNoteFormProps extends FormComponentProps {\n  info?: WizNoteConfig;\n}\n\nconst WizNoteForm: React.FC<WizNoteFormProps> = ({ form, info }) => {\n  const { verified, handleAuthentication, formRules } = useOriginForm({ form, initStatus: !!info });\n  return (\n    <React.Fragment>\n      <Form.Item\n        label={\n          <FormattedMessage id=\"backend.services.wiznote.form.origin\" defaultMessage=\"Origin\" />\n        }\n      >\n        {form.getFieldDecorator('origin', {\n          initialValue: info?.origin ?? 'https://note.wiz.cn',\n          rules: formRules,\n        })(\n          <Input.Search\n            enterButton={\n              <FormattedMessage\n                id=\"backend.services.wiznote.form.authentication\"\n                defaultMessage=\"Authentication\"\n              />\n            }\n            onSearch={handleAuthentication}\n            disabled={verified}\n          />\n        )}\n      </Form.Item>\n    </React.Fragment>\n  );\n};\n\nexport default WizNoteForm;\n"
  },
  {
    "path": "src/common/backend/services/wiznote/headerForm.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Select } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport backend from '../..';\nimport { useFetch } from '@shihengtech/hooks';\nimport WizNoteDocumentService from './service';\nimport locale from '@/common/locales';\n\nconst HeaderForm: React.FC<FormComponentProps> = ({ form: { getFieldDecorator } }) => {\n  const service = backend.getDocumentService() as WizNoteDocumentService;\n\n  const tagResponse = useFetch(async () => service.getTags(), [service], {\n    initialState: {\n      data: [],\n    },\n  });\n\n  return (\n    <Fragment>\n      <Form.Item>\n        {getFieldDecorator('tags', {\n          initialValue: [],\n        })(\n          <Select\n            mode=\"tags\"\n            maxTagCount={3}\n            style={{ width: '100%' }}\n            placeholder={locale.format({\n              id: 'backend.services.wiznote.headerForm.tags',\n              defaultMessage: 'Tags',\n            })}\n            loading={tagResponse.loading}\n          >\n            {tagResponse.data?.map(o => (\n              <Select.Option key={o.id} value={o.name} title={o.name}>\n                {o.name}\n              </Select.Option>\n            ))}\n          </Select>\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default HeaderForm;\n"
  },
  {
    "path": "src/common/backend/services/wiznote/index.ts",
    "content": "import localeService from '@/common/locales';\nimport Service from './service';\nimport Form from './form';\nimport headerForm from './headerForm';\n\nexport default () => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.wiznote.name',\n    }),\n    icon: 'wiznote',\n    type: 'WizNote',\n    headerForm,\n    service: Service,\n    form: Form,\n    permission: {\n      permissions: ['cookies'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/wiznote/interface.ts",
    "content": "import { CreateDocumentRequest } from '../interface';\n\nexport interface WizNoteConfig {\n  origin: string;\n  spaceId: number;\n}\n\nexport interface WizNoteUserInfo {\n  result: {\n    email: string;\n    userGuid: string;\n    displayName: string;\n    token: string;\n    kbGuid: string;\n  };\n}\n\nexport interface WizNoteCreateDocumentRequest extends CreateDocumentRequest {\n  tags: string[];\n}\n\nexport interface WizNoteCreateTagResponse {\n  result: {\n    tagGuid: string;\n  };\n}\n\nexport interface WizNoteGetTagsResponse {\n  result: {\n    id: string;\n    name: string;\n    tagGuid: string;\n  }[];\n}\n\nexport interface WizNoteGetRepositoriesResponse {\n  result: string[];\n}\n"
  },
  {
    "path": "src/common/backend/services/wiznote/service.ts",
    "content": "import { IWebRequestService, RequestInBackgroundOptions } from '@/service/common/webRequest';\nimport { Container } from 'typedi';\nimport {\n  WizNoteUserInfo,\n  WizNoteConfig,\n  WizNoteGetTagsResponse,\n  WizNoteCreateTagResponse,\n  WizNoteGetRepositoriesResponse,\n  WizNoteCreateDocumentRequest,\n} from '@/common/backend/services/wiznote/interface';\nimport md5 from '@web-clipper/shared/lib/md5';\nimport { DocumentService } from '@/common/backend/index';\nimport { Repository, CompleteStatus } from '../interface';\nimport { IBasicRequestService } from '@/service/common/request';\nimport { RequestHelper } from '@/service/request/common/request';\nimport { generateUuid } from '@web-clipper/shared/lib/uuid';\n\ninterface WizTempDoc {\n  docGuid: string;\n  resources: string[];\n}\n\nexport default class WizNoteDocumentService implements DocumentService {\n  private config: WizNoteConfig;\n  private webRequestService: IWebRequestService;\n  private imageRequest: RequestHelper;\n  private userInfo?: WizNoteUserInfo['result'];\n  private tempDoc?: WizTempDoc | null;\n\n  constructor(config: WizNoteConfig) {\n    this.config = config;\n    this.imageRequest = new RequestHelper({\n      baseURL: this.config.origin,\n      request: Container.get(IBasicRequestService),\n    });\n    this.webRequestService = Container.get(IWebRequestService);\n  }\n\n  getId = () => {\n    return md5(`${this.config.origin}`);\n  };\n\n  getUserInfo = async () => {\n    if (!this.userInfo) {\n      const response = await this.request<WizNoteUserInfo>(\n        '/as/user/login/auto?clientType=web&clientVersion=4.0&lang=zh-cn'\n      );\n      this.userInfo = response.result;\n    }\n    return {\n      name: this.userInfo.displayName,\n      avatar: `${this.config.origin}/as/user/avatar/${this.userInfo.userGuid}?avatarVersion=1`,\n      homePage: '',\n      description: this.userInfo.email,\n    };\n  };\n\n  getRepositories = async (): Promise<Repository[]> => {\n    await this.getUserInfo();\n\n    const response = await this.request<WizNoteGetRepositoriesResponse>(\n      `/ks/category/all/${this.userInfo!.kbGuid}`\n    );\n\n    return response.result\n      .sort((a, b) => a.localeCompare(b))\n      .map(o => {\n        return {\n          id: o,\n          name: o,\n          groupId: '为知笔记',\n          groupName: '为知笔记',\n        };\n      });\n  };\n\n  getTags = async () => {\n    const response = await this.request<WizNoteGetTagsResponse>(\n      `/ks/tag/all/${this.userInfo?.kbGuid}?clientType=web&clientVersion=4.0&lang=zh-cn`\n    );\n    return response.result;\n  };\n\n  createTag = async (name: string) => {\n    const response = await this.request<WizNoteCreateTagResponse>(\n      `/ks/tag/create/${this.userInfo?.kbGuid}?clientType=web&clientVersion=4.0&lang=zh-cn`,\n      {\n        method: 'post',\n        data: {\n          parentTagGuid: null,\n          name,\n        },\n      }\n    );\n    return response.result.tagGuid;\n  };\n\n  uploadBlob = async (blob: Blob) => {\n    if (!this.tempDoc) {\n      const response = await this.doCreateDocument({\n        kbGuid: this.userInfo?.kbGuid,\n        html: `<!DOCTYPE html><html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body></body></html>`,\n        owner: this.userInfo?.email,\n        title: `tmp-${generateUuid()}.md`,\n        params: null,\n        appInfo: null,\n      });\n      this.tempDoc = {\n        docGuid: response.result.docGuid,\n        resources: [],\n      };\n    }\n\n    const kbGuid = this.userInfo?.kbGuid ?? '';\n    const docGuid = this.tempDoc.docGuid;\n\n    const formData = new FormData();\n    formData.append('kbGuid', kbGuid);\n    formData.append('docGuid', docGuid);\n    formData.append('data', blob);\n\n    const response = await this.imageRequest.postForm<{\n      result: {\n        name: string;\n        url: string;\n      };\n    }>(`/ks/resource/upload/${kbGuid}/${docGuid}`, {\n      data: formData,\n      headers: {\n        'X-Wiz-Referer': this.config.origin,\n        'X-Wiz-Token': this.userInfo?.token ?? '',\n      },\n    });\n\n    this.tempDoc.resources.push(response.result.name);\n\n    return response.result.url;\n  };\n\n  doCreateDocument = async (data: any) => {\n    const response = await this.request<{\n      result: {\n        docGuid: string;\n      };\n    }>(`/ks/note/create/${this.userInfo?.kbGuid}?clientType=web&clientVersion=4.0&lang=zh-cn`, {\n      method: 'post',\n      data,\n    });\n    return response;\n  };\n\n  doUpdateDocument = async (data: any) => {\n    const response = await this.request<{\n      result: {\n        docGuid: string;\n      };\n    }>(\n      `/ks/note/save/${this.userInfo?.kbGuid}/${data.docGuid}?clientType=web&clientVersion=4.0&lang=zh-cn`,\n      {\n        method: 'put',\n        data,\n      }\n    );\n    return response;\n  };\n\n  createDocument = async (req: WizNoteCreateDocumentRequest): Promise<CompleteStatus> => {\n    const existTags = await this.getTags();\n    const tags = await Promise.all(\n      req.tags.map(async tag => {\n        const exist = existTags.find(o => o.name === tag);\n        if (exist) {\n          return exist.tagGuid;\n        }\n        return this.createTag(tag);\n      })\n    );\n\n    const html = `<pre>${req.content}</pre>`;\n    let response;\n    const data = {\n      kbGuid: this.userInfo?.kbGuid,\n      html: `<!DOCTYPE html><html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><style id=\"wiz_custom_css\">html, body {font-size: 12pt;}body {font-family: Helvetica, \"Hiragino Sans GB\", \"微软雅黑\", \"Microsoft YaHei UI\", SimSun, SimHei, arial, sans-serif;line-height: 1.6;margin: 0 auto;padding: 20px 16px;padding: 1.25rem 1rem;}h1, h2, h3, h4, h5, h6 {margin:20px 0 10px;margin:1.25rem 0 0.625rem;padding: 0;font-weight: bold;}h1 {font-size:20pt;font-size:1.67rem;}h2 {font-size:18pt;font-size:1.5rem;}h3 {font-size:15pt;font-size:1.25rem;}h4 {font-size:14pt;font-size:1.17rem;}h5 {font-size:12pt;font-size:1rem;}h6 {font-size:12pt;font-size:1rem;color: #777777;margin: 1rem 0;}div, p, ul, ol, dl, li {margin:0;}blockquote, table, pre, code {margin:8px 0;}ul, ol {padding-left:32px;padding-left:2rem;}ol.wiz-list-level1 > li {list-style-type:decimal;}ol.wiz-list-level2 > li {list-style-type:lower-latin;}ol.wiz-list-level3 > li {list-style-type:lower-roman;}blockquote {padding:0 12px;padding:0 0.75rem;}blockquote > :first-child {margin-top:0;}blockquote > :last-child {margin-bottom:0;}img {border:0;max-width:100%;height:auto !important;margin:2px 0;}table {border-collapse:collapse;border:1px solid #bbbbbb;}td, th {padding:4px 8px;border-collapse:collapse;border:1px solid #bbbbbb;height:28px;word-break:break-all;box-sizing: border-box;}.wiz-hide {display:none !important;}</style></head><body>${html}</body></html>`,\n      category: req.repositoryId,\n      url: req.url,\n      owner: this.userInfo?.email,\n      tags: tags.join('*'),\n      title: `${req.title}.md`,\n      params: null,\n      appInfo: null,\n    };\n    if (this.tempDoc) {\n      response = await this.doUpdateDocument({\n        ...data,\n        ...this.tempDoc,\n      });\n    } else {\n      response = await this.doCreateDocument({\n        ...data,\n      });\n      this.tempDoc = null;\n    }\n\n    return {\n      href: `${this.config.origin}/wapp/folder/${this.userInfo!.kbGuid}?c=${encodeURIComponent(\n        req.repositoryId\n      )}&docGuid=${response.result.docGuid}`,\n    };\n  };\n\n  private request<T>(\n    url: string,\n    options?: Omit<RequestInBackgroundOptions, 'headers' | 'prefix'>\n  ) {\n    return this.webRequestService.requestInBackground<T>(url, {\n      prefix: this.config.origin,\n      headers: {\n        'X-Wiz-Referer': this.config.origin,\n        'X-Wiz-Token': this.userInfo?.token ?? '',\n      },\n      ...options,\n    });\n  }\n}\n"
  },
  {
    "path": "src/common/backend/services/wolai/index.ts",
    "content": "import { ServiceMeta } from '@/common/backend';\nimport Service from './service';\n\nexport default (): ServiceMeta => {\n  return {\n    name: '我来',\n    icon: 'https://static2.wolai.com/dist/favicon.ico',\n    type: 'wolai',\n    homePage: 'https://www.wolai.com/',\n    service: Service,\n    permission: {\n      origins: ['https://api.wolai.com/*'],\n      permissions: ['cookies'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/wolai/service.ts",
    "content": "import { CompleteStatus, UnauthorizedError } from '../interface';\nimport { DocumentService, CreateDocumentRequest } from '../../index';\nimport localeService from '@/common/locales';\nimport short from 'short-uuid';\nimport { extend, RequestMethod } from 'umi-request';\nimport { WolaiRepository, WolaiUserContent, WolaiUserInfo } from './type';\nimport { IWebRequestService, WebBlockHeader } from '@/service/common/webRequest';\nimport Container from 'typedi';\nimport { ICookieService } from '@/service/common/cookie';\n\nconst PAGE = 'page';\nconst origin = 'https://api.wolai.com/';\n\nexport default class WolaiDocumentService implements DocumentService {\n  private request: RequestMethod;\n  private repositories: WolaiRepository[];\n  private userContent?: WolaiUserContent;\n  private userInfo?: WolaiUserInfo;\n  private webRequestService: IWebRequestService;\n  private cookieService: ICookieService;\n\n  constructor() {\n    const request = extend({\n      prefix: origin,\n      timeout: 10000,\n      credentials: 'include',\n    });\n    this.request = request;\n    this.repositories = [];\n    this.webRequestService = Container.get(IWebRequestService);\n    this.cookieService = Container.get(ICookieService);\n    /**\n     * TODO handle error\n     */\n    request.interceptors.response.use(\n      (response) => {\n        if (response.clone().status === 401) {\n          throw new UnauthorizedError(\n            localeService.format({\n              id: 'backend.services.wolai.unauthorizedErrorMessage',\n              defaultMessage: 'Unauthorized! Please Login Wolai Web.',\n            })\n          );\n        }\n        return response;\n      },\n      { global: false }\n    );\n  }\n\n  getUuid = () => {\n    return short.generate();\n  };\n\n  getId = () => {\n    return 'wolai';\n  };\n\n  getUserInfo = async () => {\n    if (!this.userInfo) {\n      this.userInfo = await this.fetchUserInfo();\n    }\n    const { email, userName } = this.userInfo.data;\n    return {\n      name: userName,\n      avatar: '',\n      homePage: 'https://www.wolai.com/',\n      description: email,\n    };\n  };\n\n  getRepositories = async () => {\n    if (!this.userContent) {\n      this.userContent = await this.getUserContent();\n    }\n    if (!this.userInfo) {\n      this.userInfo = await this.fetchUserInfo();\n    }\n\n    const { blocks, workspaces } = this.userContent.data;\n\n    if (!blocks) {\n      this.repositories = [];\n      return [];\n    }\n\n    const result: WolaiRepository[] = [];\n    Object.values(blocks).forEach((value) => {\n      const space = workspaces.find((workspace) => workspace.id === value.parent_id);\n      if (value.type === PAGE && !!value.attributes && !!value.attributes.title && !!space) {\n        result.push({\n          id: value.id,\n          spaceId: space.id,\n          name: value.attributes.title.toString(),\n          groupId: space.id,\n          groupName: space.name,\n          pageType: PAGE,\n        });\n      }\n    });\n\n    this.repositories = result;\n\n    return result;\n  };\n\n  createDocument = async ({\n    repositoryId,\n    title,\n    content,\n  }: CreateDocumentRequest): Promise<CompleteStatus> => {\n    const fileName = `${title}.md`;\n    const filekey = `import/${this.getUuid()}/${fileName}`;\n    const repository = this.repositories.find((o) => o.id === repositoryId);\n    if (!repository) {\n      throw new Error('Illegal repository');\n    }\n    const documentId = await this.createEmptyFile(repository, title);\n\n    const file = new File([content], filekey, {\n      type: 'text/markdown',\n    });\n    const { code, data } = await this.getFileUrl(repository, file);\n\n    if (code !== 1000) throw new Error('getSignedPostUrl error');\n\n    const formData = new FormData();\n    Object.keys(data.policyData.formData).forEach((key) => {\n      formData.append(key, data.policyData.formData[key]);\n    });\n    formData.append('key', data.fileUrl);\n    formData.append('success_action_status', '200');\n    formData.append('file', file);\n    await this.requestWithCookie(async (header) => {\n      //TODO fixme\n      return extend({}).post(await this.webRequestService.changeUrl(data.policyData.url, header), {\n        headers: {\n          [header.name]: header.value,\n        },\n        data: formData,\n      });\n    });\n    await this.requestWithCookie(async (header) => {\n      return this.request.post(\n        await this.webRequestService.changeUrl('v1/import/getImportPageData', header),\n        {\n          data: {\n            spaceId: repository.spaceId,\n            type: 'string',\n            bucket: data.policyData.bucket,\n            filename: data.fileUrl,\n            pageTitle: title,\n            pageId: documentId,\n          },\n        }\n      );\n    });\n    return {\n      href: `https://www.wolai.com/${documentId}`,\n    };\n  };\n\n  createEmptyFile = async (repository: WolaiRepository, title: string) => {\n    const documentId = this.getUuid();\n    const parentId = repository.id;\n    const spaceId = repository.spaceId;\n    const operations = {\n      requestId: this.getUuid(),\n      transactions: [\n        {\n          id: this.getUuid(),\n          operations: [\n            {\n              id: documentId,\n              table: 'wolai.block',\n              path: [],\n              command: 'set',\n              args: {\n                type: 'page',\n                id: documentId,\n                workspace_id: spaceId,\n                parent_id: parentId,\n                parent_type: 'page',\n                active: true,\n              },\n              done: true,\n            },\n            {\n              id: documentId,\n              table: 'wolai.block',\n              path: [],\n              command: 'update',\n              args: {\n                sub_nodes: '[]',\n                setting: '{}',\n                page_id: parentId,\n              },\n              done: true,\n            },\n            {\n              id: parentId,\n              table: 'wolai.block',\n              path: ['sub_nodes'],\n              command: 'listAfter',\n              args: {\n                id: documentId,\n              },\n              done: true,\n            },\n            {\n              id: documentId,\n              table: 'wolai.block',\n              path: ['attributes'],\n              command: 'update',\n              args: {\n                title: [[title]],\n              },\n              done: true,\n            },\n            {\n              id: documentId,\n              table: 'wolai.block',\n              path: [],\n              command: 'update',\n              args: {\n                type: 'page',\n              },\n              done: true,\n            },\n          ],\n        },\n      ],\n    };\n    await this.requestWithCookie(async (header) => {\n      return this.request.post(\n        await this.webRequestService.changeUrl('v1/transaction/updateChanges', header),\n        {\n          data: operations,\n        }\n      );\n    });\n    return documentId;\n  };\n\n  getFileUrl = async (repository: WolaiRepository, file: File) => {\n    return this.requestWithCookie(async (header) => {\n      // FIXME: 这里简单获取了文件后缀名，考虑到网页上的文件类型都是比较简单的，不会有类似 xxx.tar.gz 这种长后缀\n      // 构造一个合法的新文件名，避免上传接口报错\n      const fileName = `${this.getUuid()}.${file?.name?.split('.').pop()}`\n      return this.request.post(\n        await this.webRequestService.changeUrl('v1/file/getSignedPostUrl', header),\n        {\n          data: {\n            spaceId: repository.spaceId,\n            fileSize: file.size,\n            type: 'import',\n            fileName,\n          },\n        }\n      );\n    });\n  };\n\n  private getUserContent = async () => {\n    return this.requestWithCookie<WolaiUserContent>(async (header) => {\n      return this.request.post<WolaiUserContent>(\n        await this.webRequestService.changeUrl('v1/transaction/getUserData', header)\n      );\n    });\n  };\n\n  private fetchUserInfo = async () => {\n    return this.requestWithCookie<WolaiUserInfo>(async (header) => {\n      return this.request.post<WolaiUserInfo>(\n        await this.webRequestService.changeUrl('v1/authentication/user/getUserInfo', header)\n      );\n    });\n  };\n\n  /**\n   * Modify the cookie when request\n   */\n  private requestWithCookie = async <T>(\n    requestFunction: (header: WebBlockHeader) => Promise<T>\n  ) => {\n    const cookies = await this.cookieService.getAll({\n      url: origin,\n    });\n    const cookieString = cookies.map((o) => `${o.name}=${o.value}`).join(';');\n    const header = await this.webRequestService.startChangeHeader({\n      urls: [`${origin}*`],\n      requestHeaders: [\n        {\n          name: 'cookie',\n          value: cookieString,\n        },\n        {\n          name: 'origin',\n          value: 'https://www.wolai.com',\n        },\n      ],\n    });\n    try {\n      const result = await requestFunction(header);\n      await this.webRequestService.end(header);\n      return result;\n    } catch (error) {\n      await this.webRequestService.end(header);\n      throw error;\n    }\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/wolai/type.ts",
    "content": "import { Repository } from '../interface';\n\nexport interface WolaiUserContent {\n  code: number;\n  message: string;\n  data: {\n    spaceViews: {\n      [uuid: string]: {\n        id: string;\n        user_id: string;\n        workspace_id: string;\n        created_time: number;\n        notify_desktop: boolean;\n        notify_email: boolean;\n        notify_mobile: boolean;\n        favorite_pages: any[];\n      };\n    };\n    workspaces: {\n      id: string;\n      created_by: string;\n      created_time: number;\n      domain: string;\n      edited_by: string;\n      edited_time: number;\n      icon: string;\n      members: number;\n      name: string;\n      pages: string[];\n      plan_type: string;\n      team_type: string;\n    }[];\n    blocks: {\n      [uuid: string]: {\n        id: string;\n        active: boolean;\n        attributes: {\n          title?: string[][];\n        };\n        created_by: string;\n        created_time: number;\n        edited_by: string;\n        edited_time: number;\n        parent_id: string;\n        parent_type: string;\n        permissions: {\n          type: string;\n          role: string;\n          user_id: string;\n        }[];\n        sub_nodes: string[];\n        text_content: string;\n        type: string;\n        ver: number;\n        workspace_id: string;\n        setting: {};\n      };\n    };\n  };\n}\n\nexport interface WolaiUserInfo {\n  code: number;\n  data: {\n    userId: string;\n    mobile: string[];\n    email: string;\n    userName: string;\n    avatar: string;\n    userHash: string;\n    recommendCode: string;\n    registerTime: number;\n    isNewUser: boolean;\n    inviteRemainingCount: number;\n    invitedUserCount: number;\n  };\n  message: string;\n}\n\nexport interface WolaiRepository extends Repository {\n  pageType: string;\n  spaceId: string;\n}\n"
  },
  {
    "path": "src/common/backend/services/youdao/index.ts",
    "content": "import { ServiceMeta } from '@/common/backend';\nimport localeService from '@/common/locales';\nimport Service from './service';\n\nexport default (): ServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.youdao.name',\n      defaultMessage: 'Youdao',\n    }),\n    icon: 'https://note.youdao.com/web/favicon.ico',\n    type: 'youdao',\n    homePage: 'https://note.youdao.com/web/',\n    service: Service,\n    permission: {\n      origins: ['https://note.youdao.com/*'],\n      permissions: ['cookies'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/youdao/service.ts",
    "content": "import { CompleteStatus, UnauthorizedError } from './../interface';\nimport { DocumentService, Repository, CreateDocumentRequest } from '../../index';\nimport axios, { AxiosInstance } from 'axios';\nimport { stringify } from 'qs';\nimport { generateUuid } from '@web-clipper/shared/lib/uuid';\nimport localeService from '@/common/locales';\nimport Container from 'typedi';\nimport { ICookieService } from '@/service/common/cookie';\n\ninterface YouDaoRepository {\n  fileEntry: {\n    id: string;\n    name: string;\n    parentId: string;\n  };\n}\n\nexport default class YoudaoDocumentService implements DocumentService {\n  private request: AxiosInstance;\n\n  constructor() {\n    const request = axios.create({\n      baseURL: 'https://note.youdao.com',\n      timeout: 10000,\n      transformResponse: [\n        (data): any => {\n          return JSON.parse(data);\n        },\n      ],\n      withCredentials: true,\n    });\n    this.request = request;\n    this.request.interceptors.response.use(\n      r => r,\n      error => {\n        if (error.response) {\n          const { response } = error;\n          if (response.status === 500 && response.data && response.data.error === '207') {\n            return Promise.reject(\n              new UnauthorizedError(\n                localeService.format({\n                  id: 'backend.services.youdao.unauthorizedErrorMessage',\n                  defaultMessage: 'Unauthorized! Please Login Youdao Web.',\n                })\n              )\n            );\n          }\n        }\n        return Promise.reject(error);\n      }\n    );\n  }\n\n  getId = () => {\n    return 'youdao';\n  };\n\n  getRepositories = async () => {\n    const cstk = await this.getCSTK();\n    let formData = new FormData();\n    formData.append('path', '/');\n    formData.append('dirOnly', 'true');\n    formData.append('f', 'true');\n    formData.append('cstk', cstk);\n    const response = await this.request.post<YouDaoRepository[]>(\n      `/yws/api/personal/file?${stringify({\n        method: 'listEntireByParentPath',\n        keyfrom: 'web',\n        cstk,\n      })}`,\n      formData\n    );\n    return response.data.map(\n      ({ fileEntry: { parentId, name, id } }): Repository => ({\n        id,\n        name,\n        groupId: parentId,\n        groupName: localeService.format({\n          id: 'backend.services.youdao.myFolders',\n          defaultMessage: 'My Folders',\n        }),\n      })\n    );\n  };\n\n  createDocument = async ({\n    repositoryId,\n    title,\n    content,\n  }: CreateDocumentRequest): Promise<CompleteStatus> => {\n    const cstk = await this.getCSTK();\n    let formData = new FormData();\n    let uuid = generateUuid().replace(/-/g, '');\n    let fileId = `WEB${uuid}`;\n    const timestamp = String(Math.floor(Date.now() / 1000));\n    formData.append('fileId', fileId);\n    formData.append('parentId', repositoryId);\n    formData.append('name', `${title}.md`);\n    formData.append('domain', `1`);\n    formData.append('rootVersion', `-1`);\n    formData.append('dir', `false`);\n    formData.append('sessionId', '');\n    formData.append('createTime', timestamp);\n    formData.append('modifyTime', timestamp);\n    formData.append('transactionId', fileId);\n    formData.append('bodyString', content);\n    formData.append('transactionTime', timestamp);\n    formData.append('cstk', cstk);\n    try {\n      await this.request.post(\n        `/yws/api/personal/sync?${stringify({\n          method: 'push',\n          keyfrom: 'web',\n          cstk,\n        })}`,\n        formData\n      );\n    } catch (_error) {\n      uuid = generateUuid().replace(/-/g, '');\n      fileId = `WEB${uuid}`;\n      formData.set('fileId', fileId);\n      formData.set('transactionId', fileId);\n      formData.set('name', `${title}-${uuid}.md`);\n      await this.request.post(\n        `/yws/api/personal/sync?${stringify({\n          method: 'push',\n          keyfrom: 'web',\n          cstk,\n        })}`,\n        formData\n      );\n    }\n    return {\n      href: `https://note.youdao.com/web/#/file/recent/markdown/${fileId}`,\n    };\n  };\n\n  getUserInfo = async () => {\n    const cstk = await this.getCSTK();\n    const response = await this.request.get<{ name: string; photo: string }>(\n      `/yws/api/self?${stringify({\n        method: 'get',\n        keyfrom: 'web',\n        cstk,\n      })}`\n    );\n    const { data } = response;\n    return {\n      name: data.name,\n      avatar: `https://note.youdao.com${data.photo}`,\n      homePage: 'https://note.youdao.com/web',\n    };\n  };\n\n  private getCSTK = async () => {\n    const cookie = await Container.get(ICookieService).get({\n      url: 'https://note.youdao.com',\n      name: 'YNOTE_CSTK',\n    });\n    if (!cookie) {\n      throw new UnauthorizedError(\n        localeService.format({\n          id: 'backend.services.youdao.unauthorizedErrorMessage',\n          defaultMessage: 'Unauthorized! Please Login Youdao Web.',\n        })\n      );\n    }\n    return cookie.value;\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/yuque/form.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input, Select } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Component, Fragment } from 'react';\nimport { YuqueBackendServiceConfig, RepositoryType } from './interface';\nimport { FormattedMessage } from 'react-intl';\n\ninterface YuqueFormProps {\n  verified?: boolean;\n  info?: YuqueBackendServiceConfig;\n}\n\nconst RepositoryTypeOptions = [\n  {\n    key: RepositoryType.all,\n    label: (\n      <FormattedMessage\n        id=\"backend.services.yuque.form.showAllRepository\"\n        defaultMessage=\"Show All Repository\"\n      />\n    ),\n  },\n  {\n    key: RepositoryType.self,\n    label: (\n      <FormattedMessage\n        id=\"backend.services.yuque.form.showSelfRepository\"\n        defaultMessage=\"Show Self Repository\"\n      />\n    ),\n  },\n  {\n    key: RepositoryType.group,\n    label: (\n      <FormattedMessage\n        id=\"backend.services.yuque.form.showGroupRepository\"\n        defaultMessage=\"Show Group Repository\"\n      />\n    ),\n  },\n];\n\nexport default class extends Component<YuqueFormProps & FormComponentProps> {\n  render() {\n    const {\n      form: { getFieldDecorator },\n      info,\n      verified,\n    } = this.props;\n\n    let initData: Partial<YuqueBackendServiceConfig> = {\n      repositoryType: RepositoryType.self,\n    };\n    if (info) {\n      initData = info;\n    }\n    let editMode = info ? true : false;\n    return (\n      <Fragment>\n        <Form.Item label=\"AccessToken\">\n          {getFieldDecorator('accessToken', {\n            initialValue: initData.accessToken,\n            rules: [\n              {\n                required: true,\n                message: 'AccessToken is required!',\n              },\n            ],\n          })(<Input disabled={editMode || verified} />)}\n        </Form.Item>\n        <Form.Item\n          label={\n            <FormattedMessage\n              id=\"backend.services.yuque.form.repositoryType\"\n              defaultMessage=\"Repository Type\"\n            ></FormattedMessage>\n          }\n        >\n          {getFieldDecorator('repositoryType', {\n            initialValue: initData.repositoryType,\n            rules: [{ required: true, message: 'repositoryType is required!' }],\n          })(\n            <Select>\n              {RepositoryTypeOptions.map(o => (\n                <Select.Option key={o.key} value={o.key}>\n                  {o.label}\n                </Select.Option>\n              ))}\n            </Select>\n          )}\n        </Form.Item>\n      </Fragment>\n    );\n  }\n}\n"
  },
  {
    "path": "src/common/backend/services/yuque/headerForm.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport locales from '@/common/locales';\n\nconst HeaderForm: React.FC<FormComponentProps> = ({ form: { getFieldDecorator } }) => {\n  return (\n    <Fragment>\n      <Form.Item>\n        {getFieldDecorator('slug', {\n          rules: [\n            {\n              pattern: /^[\\w-.]{2,190}$/,\n              message: locales.format({\n                id: 'backend.services.yuque.headerForm.slug_error',\n              }),\n            },\n          ],\n        })(\n          <Input\n            autoComplete=\"off\"\n            placeholder={locales.format({\n              id: 'backend.services.yuque.headerForm.slug',\n            })}\n          ></Input>\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default HeaderForm;\n"
  },
  {
    "path": "src/common/backend/services/yuque/index.ts",
    "content": "import { ServiceMeta } from './../interface';\nimport Service from './service';\nimport Form from './form';\nimport localeService from '@/common/locales';\nimport headerForm from './headerForm';\n\nexport default (): ServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.yuque.name',\n      defaultMessage: 'Yuque',\n    }),\n    icon: 'yuque',\n    type: 'yuque',\n    service: Service,\n    headerForm: headerForm,\n    form: Form,\n    homePage: 'https://www.yuque.com',\n    permission: {\n      origins: ['https://www.yuque.com/*'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/yuque/interface.ts",
    "content": "import { CompleteStatus, CreateDocumentRequest, UpdateTOCRequest } from './../interface';\nimport { Repository } from '../interface';\n\nexport enum RepositoryType {\n  all = 'all',\n  self = 'self',\n  group = 'group',\n}\n\nexport interface YuqueBackendServiceConfig {\n  accessToken: string;\n  repositoryType: RepositoryType;\n}\n\nexport interface YuqueUserInfoResponse {\n  id: number;\n  avatar_url: string;\n  name: string;\n  login: string;\n  description: string;\n}\n\nexport interface YuqueGroupResponse {\n  id: number;\n  name: string;\n  login: string;\n}\n\nexport interface YuqueRepository extends Repository {\n  namespace: string;\n}\n\nexport interface YuqueRepositoryResponse {\n  id: number;\n  name: string;\n  namespace: string;\n}\n\nexport interface YuqueCreateDocumentResponse {\n  id: number;\n  slug: string;\n  title: string;\n  created_at: string;\n  updated_at: string;\n}\n\nexport interface YuqueCompleteStatus extends CompleteStatus {\n  documentId: string;\n  repositoryId: string;\n  accessToken: string;\n}\n\nexport interface YuqueCreateDocumentRequest extends CreateDocumentRequest {\n  slug?: string;\n}\n\nexport interface YuqueUpdateTOCRequest extends UpdateTOCRequest{\n  repositoryId: string;\n  documentId: number[];\n}\n"
  },
  {
    "path": "src/common/backend/services/yuque/service.ts",
    "content": "import { IBasicRequestService } from '@/service/common/request';\nimport { Container } from 'typedi';\nimport { RequestHelper } from '@/service/request/common/request';\nimport { DocumentService } from './../../index';\nimport { generateUuid } from '@web-clipper/shared/lib/uuid';\nimport * as qs from 'qs';\nimport md5 from '@web-clipper/shared/lib/md5';\nimport {\n  YuqueBackendServiceConfig,\n  YuqueUserInfoResponse,\n  RepositoryType,\n  YuqueRepositoryResponse,\n  YuqueGroupResponse,\n  YuqueCreateDocumentResponse,\n  YuqueRepository,\n  YuqueCompleteStatus,\n  YuqueCreateDocumentRequest,\n  YuqueUpdateTOCRequest,\n} from './interface';\n\nconst HOST = 'https://www.yuque.com';\nconst BASE_URL = `${HOST}/api/v2/`;\n\nexport default class YuqueDocumentService implements DocumentService {\n  private request: RequestHelper;\n  private userInfo?: YuqueUserInfoResponse;\n  private config: YuqueBackendServiceConfig;\n  private repositories: YuqueRepository[];\n\n  constructor({ accessToken, repositoryType = RepositoryType.all }: YuqueBackendServiceConfig) {\n    this.config = { accessToken, repositoryType };\n    this.request = new RequestHelper({\n      baseURL: BASE_URL,\n      headers: {\n        'X-Auth-Token': accessToken,\n      },\n      request: Container.get(IBasicRequestService),\n      interceptors: {\n        response: e => (e as any).data,\n      },\n    });\n    this.repositories = [];\n  }\n\n  getId = () => md5(this.config.accessToken);\n\n  getUserInfo = async () => {\n    if (!this.userInfo) {\n      this.userInfo = await this.getYuqueUserInfo();\n    }\n    const { avatar_url: avatar, name, login, description } = this.userInfo;\n    const homePage = `${HOST}/${login}`;\n    return {\n      avatar,\n      name,\n      homePage,\n      description,\n      login,\n    };\n  };\n\n  getRepositories = async () => {\n    let response: YuqueRepository[] = [];\n    if (this.config.repositoryType !== RepositoryType.group) {\n      if (!this.userInfo) {\n        this.userInfo = await this.getYuqueUserInfo();\n      }\n      const repos = await this.getAllRepositories(false, this.userInfo.id, this.userInfo.name);\n      response = response.concat(repos);\n    }\n    if (this.config.repositoryType !== RepositoryType.self) {\n      const groups = await this.getUserGroups();\n      for (const group of groups) {\n        const repos = await this.getAllRepositories(true, group.id, group.name);\n        response = response.concat(repos);\n      }\n    }\n    this.repositories = response;\n    return response.map(({ namespace, ...rest }) => ({ ...rest }));\n  };\n\n  createDocument = async (info: YuqueCreateDocumentRequest): Promise<YuqueCompleteStatus> => {\n    if (!this.userInfo) {\n      this.userInfo = await this.getYuqueUserInfo();\n    }\n    const { content: body, title, repositoryId } = info;\n    const repository = this.repositories.find(o => o.id === repositoryId);\n    if (!repository) {\n      throw new Error('illegal repositoryId');\n    }\n    const request = {\n      title,\n      slug: info.slug || generateUuid(),\n      body,\n      private: true,\n    };\n    const response = await this.request.post<YuqueCreateDocumentResponse>(\n      `repos/${repositoryId}/docs`,\n      {\n        data: request,\n      }\n    );\n    const data = response;\n\n    await this.updateYuqueTOC({ repositoryId, documentId: [data.id] });\n\n    return {\n      href: `${HOST}/${repository.namespace}/${data.slug}`,\n      repositoryId,\n      documentId: data.id.toString(),\n      accessToken: this.config.accessToken,\n    };\n  };\n\n  private getUserGroups = async () => {\n    if (!this.userInfo) {\n      this.userInfo = await this.getYuqueUserInfo();\n    }\n    return this.request.get<YuqueGroupResponse[]>(`users/${this.userInfo.login}/groups`);\n  };\n\n  private getYuqueUserInfo = async () => {\n    return this.request.get<YuqueUserInfoResponse>('user');\n  };\n\n  private getAllRepositories = async (isGroup: boolean, groupId: number, groupName: string) => {\n    let offset = 0;\n    let result = await this.getYuqueRepositories(offset, isGroup, String(groupId));\n    while (result.length - offset === 20) {\n      offset = offset + 20;\n      result = result.concat(await this.getYuqueRepositories(offset, isGroup, String(groupId)));\n    }\n    return result.map(\n      ({ id, name, namespace }): YuqueRepository => ({\n        id: String(id),\n        name,\n        groupId: String(groupId),\n        groupName: groupName,\n        namespace,\n      })\n    );\n  };\n\n  private getYuqueRepositories = async (offset: number, isGroup: boolean, slug: string) => {\n    const query = {\n      offset: offset,\n    };\n    try {\n      const response = await this.request.get<YuqueRepositoryResponse[]>(\n        `${isGroup ? 'groups' : 'users'}/${slug}/repos?${qs.stringify(query)}`\n      );\n      return response;\n    } catch (_error) {\n      return [];\n    }\n  };\n\n  private updateYuqueTOC = async (info: YuqueUpdateTOCRequest) => {\n    const { repositoryId, documentId } = info;\n    const requestBody = {\n      action: 'prependNode',\n      action_mode: 'child',\n      doc_ids: documentId,\n      type: 'DOC',\n    };\n\n    try {\n      const response = await this.request.put(`repos/${repositoryId}/toc`, {\n        data: requestBody,\n\t\t\t});\n      return response;\n    } catch (_error) {\n      return {};\n    }\n\n  };\n}\n"
  },
  {
    "path": "src/common/backend/services/yuque_oauth/form.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input, Select } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Component, Fragment } from 'react';\nimport { YuqueBackendServiceConfig, RepositoryType } from './interface';\nimport { FormattedMessage } from 'react-intl';\n\ninterface YuqueFormProps {\n  verified?: boolean;\n  info?: YuqueBackendServiceConfig;\n}\n\nconst RepositoryTypeOptions = [\n  {\n    key: RepositoryType.all,\n    label: (\n      <FormattedMessage\n        id=\"backend.services.yuque.form.showAllRepository\"\n        defaultMessage=\"Show All Repository\"\n      />\n    ),\n  },\n  {\n    key: RepositoryType.self,\n    label: (\n      <FormattedMessage\n        id=\"backend.services.yuque.form.showSelfRepository\"\n        defaultMessage=\"Show Self Repository\"\n      />\n    ),\n  },\n  {\n    key: RepositoryType.group,\n    label: (\n      <FormattedMessage\n        id=\"backend.services.yuque.form.showGroupRepository\"\n        defaultMessage=\"Show Group Repository\"\n      />\n    ),\n  },\n];\n\nexport default class extends Component<YuqueFormProps & FormComponentProps> {\n  render() {\n    const {\n      form: { getFieldDecorator },\n      info,\n      verified,\n    } = this.props;\n\n    let initData: Partial<YuqueBackendServiceConfig> = {\n      repositoryType: RepositoryType.self,\n    };\n    if (info) {\n      initData = info;\n    }\n    let editMode = info ? true : false;\n    return (\n      <Fragment>\n        <Form.Item label=\"AccessToken\">\n          {getFieldDecorator('access_token', {\n            initialValue: initData.access_token,\n            rules: [\n              {\n                required: true,\n                message: 'AccessToken is required!',\n              },\n            ],\n          })(<Input disabled={editMode || verified} />)}\n        </Form.Item>\n        <Form.Item\n          label={\n            <FormattedMessage\n              id=\"backend.services.yuque.form.repositoryType\"\n              defaultMessage=\"Repository Type\"\n            ></FormattedMessage>\n          }\n        >\n          {getFieldDecorator('repositoryType', {\n            initialValue: initData.repositoryType || RepositoryType.all,\n            rules: [{ required: true, message: 'repositoryType is required!' }],\n          })(\n            <Select>\n              {RepositoryTypeOptions.map(o => (\n                <Select.Option key={o.key} value={o.key}>\n                  {o.label}\n                </Select.Option>\n              ))}\n            </Select>\n          )}\n        </Form.Item>\n      </Fragment>\n    );\n  }\n}\n"
  },
  {
    "path": "src/common/backend/services/yuque_oauth/headerForm.tsx",
    "content": "import { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { Fragment } from 'react';\nimport locales from '@/common/locales';\n\nconst HeaderForm: React.FC<FormComponentProps> = ({ form: { getFieldDecorator } }) => {\n  return (\n    <Fragment>\n      <Form.Item>\n        {getFieldDecorator('slug', {\n          rules: [\n            {\n              pattern: /^[\\w-.]{2,190}$/,\n              message: locales.format({\n                id: 'backend.services.yuque.headerForm.slug_error',\n              }),\n            },\n          ],\n        })(\n          <Input\n            autoComplete=\"off\"\n            placeholder={locales.format({\n              id: 'backend.services.yuque.headerForm.slug',\n            })}\n          ></Input>\n        )}\n      </Form.Item>\n    </Fragment>\n  );\n};\n\nexport default HeaderForm;\n"
  },
  {
    "path": "src/common/backend/services/yuque_oauth/index.ts",
    "content": "import config from '@/config';\nimport { ServiceMeta } from './../interface';\nimport Service from './service';\nimport localeService from '@/common/locales';\nimport { stringify } from 'qs';\nimport form from './form';\nimport headerForm from './headerForm';\nimport { IConfigService } from '@/service/common/config';\nimport { Container } from 'typedi';\n\nconst oauthUrl = `https://www.yuque.com/oauth2/authorize?${stringify({\n  client_id: config.yuqueClientId,\n  scope: config.yuqueScope,\n  redirect_uri: config.yuqueCallback,\n  state: Container.get(IConfigService).id,\n  response_type: 'code',\n})}`;\n\nexport default (): ServiceMeta => {\n  return {\n    name: localeService.format({\n      id: 'backend.services.yuque_oauth.name',\n    }),\n    icon: 'yuque',\n    type: 'yuque_oauth',\n    headerForm: headerForm,\n    service: Service,\n    oauthUrl,\n    form: form,\n    homePage: 'https://www.yuque.com',\n    permission: {\n      origins: ['https://www.yuque.com/*'],\n    },\n  };\n};\n"
  },
  {
    "path": "src/common/backend/services/yuque_oauth/interface.ts",
    "content": "import { CompleteStatus, CreateDocumentRequest } from './../interface';\nimport { Repository } from '../interface';\n\nexport enum RepositoryType {\n  all = 'all',\n  self = 'self',\n  group = 'group',\n}\n\nexport interface YuqueBackendServiceConfig {\n  access_token: string;\n  repositoryType: RepositoryType;\n}\n\nexport interface YuqueUserInfoResponse {\n  id: number;\n  avatar_url: string;\n  name: string;\n  login: string;\n  description: string;\n}\n\nexport interface YuqueGroupResponse {\n  id: number;\n  name: string;\n  login: string;\n}\n\nexport interface YuqueRepository extends Repository {\n  namespace: string;\n}\n\nexport interface YuqueRepositoryResponse {\n  id: number;\n  name: string;\n  namespace: string;\n}\n\nexport interface YuqueCreateDocumentResponse {\n  id: number;\n  slug: string;\n  title: string;\n  created_at: string;\n  updated_at: string;\n}\n\nexport interface YuqueCompleteStatus extends CompleteStatus {\n  documentId: string;\n  repositoryId: string;\n}\n\nexport interface YuqueCreateDocumentRequest extends CreateDocumentRequest {\n  slug?: string;\n}\n"
  },
  {
    "path": "src/common/backend/services/yuque_oauth/service.ts",
    "content": "import { RequestHelper } from './../../../../service/request/common/request';\nimport { IBasicRequestService } from './../../../../service/common/request';\nimport { Container } from 'typedi';\nimport { DocumentService } from './../../index';\nimport { generateUuid } from '@web-clipper/shared/lib/uuid';\nimport * as qs from 'qs';\nimport md5 from '@web-clipper/shared/lib/md5';\nimport {\n  YuqueBackendServiceConfig,\n  YuqueUserInfoResponse,\n  RepositoryType,\n  YuqueRepositoryResponse,\n  YuqueGroupResponse,\n  YuqueCreateDocumentResponse,\n  YuqueRepository,\n  YuqueCompleteStatus,\n  YuqueCreateDocumentRequest,\n} from './interface';\n\nconst HOST = 'https://www.yuque.com';\nconst BASE_URL = `${HOST}/api/v2/`;\n\nexport default class YuqueDocumentService implements DocumentService {\n  private request: RequestHelper;\n  private userInfo?: YuqueUserInfoResponse;\n  private config: YuqueBackendServiceConfig;\n  private repositories: YuqueRepository[];\n\n  constructor({ access_token, repositoryType = RepositoryType.all }: YuqueBackendServiceConfig) {\n    this.config = { access_token, repositoryType };\n    this.request = new RequestHelper({\n      baseURL: BASE_URL,\n      headers: {\n        'X-Auth-Token': access_token,\n      },\n      request: Container.get(IBasicRequestService),\n      interceptors: {\n        response: e => (e as any).data,\n      },\n    });\n    this.repositories = [];\n  }\n\n  getId = () => md5(this.config.access_token);\n\n  getUserInfo = async () => {\n    if (!this.userInfo) {\n      this.userInfo = await this.getYuqueUserInfo();\n    }\n    const { avatar_url: avatar, name, login, description } = this.userInfo;\n    const homePage = `${HOST}/${login}`;\n    return {\n      avatar,\n      name,\n      homePage,\n      description,\n      login,\n    };\n  };\n\n  getRepositories = async () => {\n    let response: YuqueRepository[] = [];\n    if (this.config.repositoryType !== RepositoryType.group) {\n      if (!this.userInfo) {\n        this.userInfo = await this.getYuqueUserInfo();\n      }\n      const repos = await this.getAllRepositories(false, this.userInfo.id, this.userInfo.name);\n      response = response.concat(repos);\n    }\n    if (this.config.repositoryType !== RepositoryType.self) {\n      const groups = await this.getUserGroups();\n      for (const group of groups) {\n        const repos = await this.getAllRepositories(true, group.id, group.name);\n        response = response.concat(repos);\n      }\n    }\n    this.repositories = response;\n    return response.map(({ namespace, ...rest }) => ({ ...rest }));\n  };\n\n  createDocument = async (info: YuqueCreateDocumentRequest): Promise<YuqueCompleteStatus> => {\n    if (!this.userInfo) {\n      this.userInfo = await this.getYuqueUserInfo();\n    }\n    const { content: body, title, repositoryId } = info;\n    const repository = this.repositories.find(o => o.id === repositoryId);\n    if (!repository) {\n      throw new Error('illegal repositoryId');\n    }\n    const request = {\n      title,\n      slug: info.slug || generateUuid(),\n      body,\n      private: true,\n    };\n    const response = await this.request.post<YuqueCreateDocumentResponse>(\n      `repos/${repositoryId}/docs`,\n      {\n        data: request,\n      }\n    );\n    const data = response;\n    return {\n      href: `${HOST}/${repository.namespace}/${data.slug}`,\n      repositoryId,\n      documentId: data.id.toString(),\n    };\n  };\n\n  private getUserGroups = async () => {\n    if (!this.userInfo) {\n      this.userInfo = await this.getYuqueUserInfo();\n    }\n    return this.request.get<YuqueGroupResponse[]>(`users/${this.userInfo.login}/groups`);\n  };\n\n  private getYuqueUserInfo = async () => {\n    const response = await this.request.get<YuqueUserInfoResponse>('user');\n    return response;\n  };\n\n  private getAllRepositories = async (isGroup: boolean, groupId: number, groupName: string) => {\n    let offset = 0;\n    let result = await this.getYuqueRepositories(offset, isGroup, String(groupId));\n    while (result.length - offset === 20) {\n      offset = offset + 20;\n      result = result.concat(await this.getYuqueRepositories(offset, isGroup, String(groupId)));\n    }\n    return result.map(\n      ({ id, name, namespace }): YuqueRepository => ({\n        id: String(id),\n        name,\n        groupId: String(groupId),\n        groupName: groupName,\n        namespace,\n      })\n    );\n  };\n\n  private getYuqueRepositories = async (offset: number, isGroup: boolean, slug: string) => {\n    const query = {\n      offset: offset,\n    };\n    try {\n      const response = await this.request.get<YuqueRepositoryResponse[]>(\n        `${isGroup ? 'groups' : 'users'}/${slug}/repos?${qs.stringify(query)}`\n      );\n      return response;\n    } catch (error) {\n      console.log(error);\n      return [];\n    }\n  };\n}\n"
  },
  {
    "path": "src/common/blob.ts",
    "content": "const Base64ImageToBlob = (image: string): Blob => {\n  const arr = image.split(',');\n  const mime = arr[0].match(/:(.*?);/)![1] || 'image/png';\n  const bytes = window.atob(arr[1]);\n  let ab = new ArrayBuffer(bytes.length);\n  let ia = new Uint8Array(ab);\n  for (let i = 0; i < bytes.length; i++) {\n    ia[i] = bytes.charCodeAt(i);\n  }\n  const blob = new Blob([ab], {\n    type: mime,\n  });\n  return blob;\n};\n\nconst BlobToBase64 = (blob: Blob): Promise<string> => {\n  const reader = new FileReader();\n  reader.readAsDataURL(blob);\n  return new Promise(resolve => {\n    reader.onloadend = () => {\n      resolve(reader.result as string);\n    };\n  });\n};\n\nfunction loadImage(date: string): Promise<HTMLImageElement> {\n  return new Promise<HTMLImageElement>((resolve, reject) => {\n    let img = new Image();\n    img.onload = () => resolve(img);\n    img.onerror = reject;\n    img.src = date;\n  });\n}\n\nexport { Base64ImageToBlob, loadImage, BlobToBase64 };\n"
  },
  {
    "path": "src/common/buffer.ts",
    "content": "/* eslint-disable @typescript-eslint/member-ordering */\n/* eslint-disable no-dupe-class-members */\n/* eslint-disable no-param-reassign */\n/*---------------------------------------------------------------------------------------------\n *  Copyright (c) Microsoft Corporation. All rights reserved.\n *  Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nimport * as strings from './strings';\n\ndeclare const Buffer: any;\n\nconst hasBuffer = typeof Buffer !== 'undefined';\nconst hasTextEncoder = typeof TextEncoder !== 'undefined';\nconst hasTextDecoder = typeof TextDecoder !== 'undefined';\n\nlet textEncoder: TextEncoder | null;\nlet textDecoder: TextDecoder | null;\n\nexport class VSBuffer {\n  static alloc(byteLength: number): VSBuffer {\n    if (hasBuffer) {\n      return new VSBuffer(Buffer.allocUnsafe(byteLength));\n    }\n    return new VSBuffer(new Uint8Array(byteLength));\n  }\n\n  static wrap(actual: Uint8Array): VSBuffer {\n    if (hasBuffer && !Buffer.isBuffer(actual)) {\n      // https://nodejs.org/dist/latest-v10.x/docs/api/buffer.html#buffer_class_method_buffer_from_arraybuffer_byteoffset_length\n      // Create a zero-copy Buffer wrapper around the ArrayBuffer pointed to by the Uint8Array\n      actual = Buffer.from(actual.buffer, actual.byteOffset, actual.byteLength);\n    }\n    return new VSBuffer(actual);\n  }\n\n  static fromString(source: string, options?: { dontUseNodeBuffer?: boolean }): VSBuffer {\n    const dontUseNodeBuffer = options?.dontUseNodeBuffer || false;\n    if (!dontUseNodeBuffer && hasBuffer) {\n      return new VSBuffer(Buffer.from(source));\n    }\n    if (hasTextEncoder) {\n      if (!textEncoder) {\n        textEncoder = new TextEncoder();\n      }\n      return new VSBuffer(textEncoder.encode(source));\n    }\n    return new VSBuffer(strings.encodeUTF8(source));\n  }\n\n  static concat(buffers: VSBuffer[], totalLength?: number): VSBuffer {\n    if (typeof totalLength === 'undefined') {\n      totalLength = 0;\n      for (let i = 0, len = buffers.length; i < len; i++) {\n        totalLength += buffers[i].byteLength;\n      }\n    }\n\n    const ret = VSBuffer.alloc(totalLength);\n    let offset = 0;\n    for (let i = 0, len = buffers.length; i < len; i++) {\n      const element = buffers[i];\n      ret.set(element, offset);\n      offset += element.byteLength;\n    }\n\n    return ret;\n  }\n\n  readonly buffer: Uint8Array;\n  readonly byteLength: number;\n\n  private constructor(buffer: Uint8Array) {\n    this.buffer = buffer;\n    this.byteLength = this.buffer.byteLength;\n  }\n\n  toString(): string {\n    if (hasBuffer) {\n      return this.buffer.toString();\n    }\n    if (hasTextDecoder) {\n      if (!textDecoder) {\n        textDecoder = new TextDecoder();\n      }\n      return textDecoder.decode(this.buffer);\n    }\n    return strings.decodeUTF8(this.buffer);\n  }\n\n  slice(start?: number, end?: number): VSBuffer {\n    // IMPORTANT: use subarray instead of slice because TypedArray#slice\n    // creates shallow copy and NodeBuffer#slice doesn't. The use of subarray\n    // ensures the same, performant, behaviour.\n    return new VSBuffer(this.buffer.subarray(start! /*bad lib.d.ts*/, end));\n  }\n\n  set(array: VSBuffer, offset?: number): void;\n  set(array: Uint8Array, offset?: number): void;\n  set(array: VSBuffer | Uint8Array, offset?: number): void {\n    if (array instanceof VSBuffer) {\n      this.buffer.set(array.buffer, offset);\n    } else {\n      this.buffer.set(array, offset);\n    }\n  }\n\n  readUInt32BE(offset: number): number {\n    return readUInt32BE(this.buffer, offset);\n  }\n\n  writeUInt32BE(value: number, offset: number): void {\n    writeUInt32BE(this.buffer, value, offset);\n  }\n\n  readUInt32LE(offset: number): number {\n    return readUInt32LE(this.buffer, offset);\n  }\n\n  writeUInt32LE(value: number, offset: number): void {\n    writeUInt32LE(this.buffer, value, offset);\n  }\n\n  readUInt8(offset: number): number {\n    return readUInt8(this.buffer, offset);\n  }\n\n  writeUInt8(value: number, offset: number): void {\n    writeUInt8(this.buffer, value, offset);\n  }\n}\n\nexport function readUInt16LE(source: Uint8Array, offset: number): number {\n  return ((source[offset + 0] << 0) >>> 0) | ((source[offset + 1] << 8) >>> 0);\n}\n\nexport function writeUInt16LE(destination: Uint8Array, value: number, offset: number): void {\n  destination[offset + 0] = value & 0b11111111;\n  value = value >>> 8;\n  destination[offset + 1] = value & 0b11111111;\n}\n\nexport function readUInt32BE(source: Uint8Array, offset: number): number {\n  return (\n    source[offset] * 2 ** 24 +\n    source[offset + 1] * 2 ** 16 +\n    source[offset + 2] * 2 ** 8 +\n    source[offset + 3]\n  );\n}\n\nexport function writeUInt32BE(destination: Uint8Array, value: number, offset: number): void {\n  destination[offset + 3] = value;\n  value = value >>> 8;\n  destination[offset + 2] = value;\n  value = value >>> 8;\n  destination[offset + 1] = value;\n  value = value >>> 8;\n  destination[offset] = value;\n}\n\nexport function readUInt32LE(source: Uint8Array, offset: number): number {\n  return (\n    ((source[offset + 0] << 0) >>> 0) |\n    ((source[offset + 1] << 8) >>> 0) |\n    ((source[offset + 2] << 16) >>> 0) |\n    ((source[offset + 3] << 24) >>> 0)\n  );\n}\n\nexport function writeUInt32LE(destination: Uint8Array, value: number, offset: number): void {\n  destination[offset + 0] = value & 0b11111111;\n  value = value >>> 8;\n  destination[offset + 1] = value & 0b11111111;\n  value = value >>> 8;\n  destination[offset + 2] = value & 0b11111111;\n  value = value >>> 8;\n  destination[offset + 3] = value & 0b11111111;\n}\n\nexport function readUInt8(source: Uint8Array, offset: number): number {\n  return source[offset];\n}\n\nexport function writeUInt8(destination: Uint8Array, value: number, offset: number): void {\n  destination[offset] = value;\n}\n"
  },
  {
    "path": "src/common/chrome/storage.ts",
    "content": "import { AbstractStorageService } from '@web-clipper/shared/lib/storage';\nimport * as browser from '@web-clipper/chrome-promise';\n\nclass LocalStorageService extends AbstractStorageService {\n  constructor() {\n    super(browser.storage.local, browser.storage.onChanged, 'local');\n  }\n}\n\nclass SyncStorageService extends AbstractStorageService {\n  constructor() {\n    super(browser.storage.sync, browser.storage.onChanged, 'sync');\n  }\n}\n\nconst localStorageService = new LocalStorageService();\nconst syncStorageService = new SyncStorageService();\n\nexport { localStorageService, syncStorageService };\n"
  },
  {
    "path": "src/common/error.ts",
    "content": "export interface SerializedError {\n  readonly $isError: true;\n  readonly name: string;\n  readonly message: string;\n  readonly stack: string;\n}\n\nexport function transformErrorForSerialization(error: Error): SerializedError;\nexport function transformErrorForSerialization(error: any): any;\nexport function transformErrorForSerialization(error: any): any {\n  if (error instanceof Error) {\n    let { name, message } = error;\n    const stack: string = (<any>error).stacktrace || (<any>error).stack;\n    return {\n      $isError: true,\n      name,\n      message,\n      stack,\n    };\n  }\n\n  // return as is\n  return error;\n}\n"
  },
  {
    "path": "src/common/getResource.ts",
    "content": "export function getResourcePath(name: string) {\n  let isFirefox = chrome.runtime.getURL(name).startsWith('moz-extension');\n  if (isFirefox) {\n    return `chrome/${name}`;\n  }\n  return name;\n}\n"
  },
  {
    "path": "src/common/hooks/useFilterExtensions.ts",
    "content": "import { useMemo } from 'react';\nimport { ExtensionType, SerializedExtensionInfo } from '@/extensions/common';\n\nconst useFilterExtensions = <T extends SerializedExtensionInfo>(extensions: T[]) => {\n  return useMemo(() => {\n    const toolExtensions: T[] = [];\n    const clipExtensions: T[] = [];\n    extensions.forEach(o => {\n      if (o.type === ExtensionType.Tool) {\n        toolExtensions.push(o);\n        return;\n      }\n      clipExtensions.push(o);\n    });\n    return [toolExtensions, clipExtensions];\n  }, [extensions]);\n};\n\nexport default useFilterExtensions;\n"
  },
  {
    "path": "src/common/hooks/useFilterImageHostingServices.ts",
    "content": "import { ImageHostingServiceMeta } from '../backend';\nimport { ImageHosting } from '../modelTypes/userPreference';\n\ninterface Props {\n  backendServiceType: string;\n  imageHostingServices: ImageHosting[];\n  imageHostingServicesMap: {\n    [type: string]: ImageHostingServiceMeta;\n  };\n}\n\nexport type ImageHostingWithMeta = {\n  imageHostingServices: ImageHosting;\n  meta: ImageHostingServiceMeta;\n};\n\nconst useFilterImageHostingServices = ({\n  backendServiceType,\n  imageHostingServices,\n  imageHostingServicesMap,\n}: Props) => {\n  return imageHostingServices\n    .map(o => {\n      const meta = imageHostingServicesMap[o.type];\n      if (!meta) {\n        return null;\n      }\n      if (meta.builtIn && meta.type !== backendServiceType) {\n        return null;\n      }\n      if (meta.support && !meta.support(backendServiceType)) {\n        return null;\n      }\n      return { imageHostingServices: o, meta };\n    })\n    .filter((o): o is ImageHostingWithMeta => !!o);\n};\n\nexport default useFilterImageHostingServices;\n"
  },
  {
    "path": "src/common/hooks/useOriginPermission.ts",
    "content": "import { IPermissionsService } from '@/service/common/permissions';\nimport { useState } from 'react';\nimport Container from 'typedi';\n\nconst useOriginPermission = (initData: boolean) => {\n  const [verified, setVerified] = useState(initData);\n  const permissionsService = Container.get(IPermissionsService);\n  const requestOriginPermission = async (origin: string) => {\n    const result = await permissionsService.request({\n      origins: [`${origin}/*`],\n    });\n    setVerified(result);\n  };\n  return [verified, requestOriginPermission] as const;\n};\n\nexport default useOriginPermission;\n"
  },
  {
    "path": "src/common/hooks/useVerifiedAccount.tsx",
    "content": "import { UserPreferenceStore } from '@/common/types';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport React, { useState, useMemo, useCallback, useEffect, useRef } from 'react';\nimport { omit, isEqual } from 'lodash';\nimport { FormattedMessage } from 'react-intl';\nimport { message } from 'antd';\nimport { useFetch } from '@shihengtech/hooks';\n\ntype UseVerifiedAccountProps = FormComponentProps & {\n  services: UserPreferenceStore['servicesMeta'];\n  initAccount?: any;\n};\n\nfunction useDeepCompareMemoize<T>(value: T) {\n  const ref = React.useRef<T>();\n  if (!isEqual(value, ref.current)) {\n    ref.current = value;\n  }\n  return ref.current;\n}\n\nconst useVerifiedAccount = ({ form, services, initAccount }: UseVerifiedAccountProps) => {\n  const [type, _setType] = useState<string>(\n    initAccount ? initAccount.type : Object.values(services)[0].type\n  );\n  const service = services[type];\n  const changeType = (type: string) => {\n    _setType(type);\n    const values = form.getFieldsValue();\n    form.resetFields(Object.keys(omit(values, ['type'])));\n  };\n  const { data, run, loading } = useFetch(\n    async (info: any) => {\n      const Service = service.service;\n      const instance = new Service(info);\n      const userInfo = await instance.getUserInfo();\n      const repositories = await instance.getRepositories();\n      const id = await instance.getId();\n      return { userInfo, repositories, id };\n    },\n    [service],\n    {\n      auto: false,\n      onError: e => {\n        message.error(e.message);\n      },\n    }\n  );\n\n  let loadAccount = useCallback(() => {\n    form.validateFields((error, values) => {\n      if (error) {\n        return;\n      }\n      const { type, defaultRepositoryId, imageHosting, ...info } = values;\n      run(info);\n    });\n  }, [form, run]);\n\n  const accountStatus = {\n    repositories: data?.repositories ?? [],\n    userInfo: data?.userInfo ?? null,\n    verified: !!data && !loading,\n    id: data?.id ?? null,\n  };\n\n  let serviceForm = useMemo(() => {\n    if (!service.form) {\n      return null;\n    }\n    return (\n      <service.form\n        form={form}\n        verified={accountStatus.verified}\n        info={initAccount}\n        loadAccount={loadAccount}\n      />\n    );\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [accountStatus.verified, form, initAccount, loadAccount, service.form]);\n\n  const okText = useMemo(() => {\n    if (loading) {\n      return <FormattedMessage id=\"preference.accountList.verifying\" defaultMessage=\"Verifying\" />;\n    }\n    return accountStatus.verified ? (\n      <FormattedMessage id=\"preference.accountList.add\" defaultMessage=\"Add\" />\n    ) : (\n      <FormattedMessage id=\"preference.accountList.verify\" defaultMessage=\"Verify\" />\n    );\n  }, [accountStatus.verified, loading]);\n\n  let oauthLink = useMemo(() => {\n    return service.oauthUrl ? (\n      <a href={service.oauthUrl} target=\"_blank\">\n        <FormattedMessage id=\"preference.accountList.login\" defaultMessage=\"Login\" />\n      </a>\n    ) : null;\n  }, [service.oauthUrl]);\n\n  const _formInfo = useMemo(() => {\n    const values = form.getFieldsValue();\n    const { defaultRepositoryId, type: curT, imageHosting, ...info } = values;\n    if (type !== curT) {\n      return null;\n    }\n    return info;\n  }, [form, type]);\n\n  const formInfo = useDeepCompareMemoize(_formInfo);\n  const verifiedRef = useRef(accountStatus.verified);\n  verifiedRef.current = accountStatus.verified;\n\n  useEffect(() => {\n    if (!verifiedRef.current || !formInfo) {\n      return;\n    }\n    run(formInfo);\n  }, [verifiedRef, formInfo, run, form]);\n\n  return {\n    type,\n    service,\n    accountStatus: accountStatus,\n    verifying: loading,\n    verifyAccount: run,\n    loadAccount,\n    changeType,\n    serviceForm,\n    okText,\n    oauthLink,\n  };\n};\nexport default useVerifiedAccount;\n"
  },
  {
    "path": "src/common/loading.test.ts",
    "content": "/* eslint-disable no-loop-func */\nimport { loading, loadingStatus } from './loading';\nimport { autorun, action, observable } from 'mobx';\nimport * as vitest from 'vitest';\n\nfunction flushPromises() {\n  return new Promise(resolve => setImmediate(resolve));\n}\n\nvitest.vi.useFakeTimers();\n\nclass Test {\n  @observable\n  public actionAfterLoadingCount = 0;\n\n  @observable\n  public actionBeforeLoadingCount = 0;\n\n  @loading\n  exec = (time: number) => {\n    return new Promise(r => setTimeout(r, time));\n  };\n\n  @action\n  @loading\n  actionAfterLoading() {\n    this.actionAfterLoadingCount++;\n    this.actionAfterLoadingCount++;\n  }\n\n  @loading\n  @action\n  actionBeforeLoading() {\n    this.actionBeforeLoadingCount++;\n    this.actionBeforeLoadingCount++;\n  }\n}\n\ndescribe.skip('test loading decorator', () => {\n  beforeEach(() => {\n    vitest.vi.useFakeTimers();\n  });\n  it('test race condition', async () => {\n    const instance = new Test();\n    instance.exec(3000);\n    instance.exec(9000);\n\n    await vitest.vi.advanceTimersByTime(5000);\n    expect(loadingStatus(instance).exec).toBe(true);\n    await vitest.vi.advanceTimersByTime(5000);\n    await flushPromises();\n    expect(loadingStatus(instance).exec).toBe(false);\n  });\n\n  it('test auto run', async () => {\n    const instance = new Test();\n    const log = vitest.vi.fn();\n    instance.exec(3000);\n    autorun(() => {\n      log(loadingStatus(instance).exec);\n    });\n    await vitest.vi.advanceTimersByTime(1000);\n    expect(log).toBeCalledTimes(1);\n    expect(log).toHaveBeenLastCalledWith(true);\n    await vitest.vi.advanceTimersByTime(3000);\n    await flushPromises();\n    expect(log).toBeCalledTimes(2);\n    expect(log).toHaveBeenLastCalledWith(false);\n  });\n\n  describe('should work correct with action', () => {\n    it('actionBeforeLoading ', async () => {\n      const instance = new Test();\n      const result: string[] = [];\n      const log = (daa: string) => {\n        result.push(daa);\n      };\n      autorun(() => {\n        const loading = loadingStatus(instance).actionBeforeLoading;\n        const count = instance.actionBeforeLoadingCount;\n        log(`${loading}-${count}`);\n      });\n      instance.actionBeforeLoading();\n      expect(loadingStatus(instance).actionBeforeLoading).toBe(true);\n      await vitest.vi.advanceTimersByTime(1000);\n      expect(loadingStatus(instance).actionBeforeLoading).toBe(false);\n      expect(result).toEqual(['undefined-0', 'true-0', 'true-2', 'false-2']);\n    });\n\n    it('actionAfterLoading', async () => {\n      const instance = new Test();\n      const result: string[] = [];\n      const log = (daa: string) => {\n        result.push(daa);\n      };\n      autorun(() => {\n        const loading = loadingStatus(instance).actionAfterLoading;\n        const count = instance.actionAfterLoadingCount;\n        log(`${loading}-${count}`);\n      });\n      instance.actionAfterLoading();\n      expect(loadingStatus(instance).actionAfterLoading).toBe(true);\n      await vitest.vi.advanceTimersByTime(1000);\n      expect(loadingStatus(instance).actionAfterLoading).toBe(false);\n      expect(result).toEqual(['undefined-0', 'true-2', 'false-2']);\n    });\n  });\n});\n"
  },
  {
    "path": "src/common/loading.ts",
    "content": "import { observable } from 'mobx';\nimport { generateUuid } from '@web-clipper/shared/lib/uuid';\n\ntype FunctionKeys<T> = {\n  [K in keyof T]: T[K] extends Function ? K : never;\n}[keyof T];\n\nconst loadingMap = observable.map<string, boolean>();\n\nconst cache = new Map<any, any>();\n\nexport function loadingStatus<T>(\n  instance: T\n): {\n  [P in FunctionKeys<T>]: boolean;\n} {\n  if (!cache.has(instance)) {\n    const result = {};\n    Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).forEach(key => {\n      Object.defineProperty(result, key, {\n        get: () => {\n          const uuid = Reflect.getMetadata(`loading:${key}`, instance);\n          if (!uuid) {\n            throw new Error();\n          }\n          return loadingMap.get(uuid);\n        },\n      });\n    });\n    cache.set(instance, result);\n  }\n  return cache.get(instance);\n}\n\nfunction LoadingHoc(uuidKey: string, fn: Function) {\n  let execCount = 0;\n  return async function() {\n    const execCountCache = execCount + 1;\n    execCount = execCountCache;\n    try {\n      loadingMap.set(uuidKey, true);\n      //@ts-ignore\n      return await fn.apply(this, arguments);\n    } catch (err) {\n      throw err;\n    } finally {\n      if (execCountCache === execCount) {\n        loadingMap.set(uuidKey, false);\n      }\n    }\n  };\n}\n\nexport function loading(target: any, key: string, descriptor?: any) {\n  const uuidKey = generateUuid();\n  Reflect.defineMetadata(`loading:${key}`, uuidKey, target);\n\n  if (descriptor) {\n    descriptor.value = LoadingHoc(uuidKey, descriptor.value);\n  } else {\n    Object.defineProperty(target, key, {\n      enumerable: false,\n      configurable: true,\n      set(v: any) {\n        Object.defineProperty(this, key, {\n          enumerable: false,\n          writable: true,\n          configurable: true,\n          value: LoadingHoc(uuidKey, v),\n        });\n      },\n      get() {\n        // eslint-disable-next-line no-undefined\n        return undefined;\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "src/common/locales/antd.ts",
    "content": "import en_US from 'antd/lib/locale-provider/en_US';\nimport ja_JP from 'antd/lib/locale-provider/ja_JP';\nimport ru_RU from 'antd/lib/locale-provider/ru_RU';\nimport zh_CN from 'antd/lib/locale-provider/zh_CN';\nimport zh_TW from 'antd/lib/locale-provider/zh_TW';\n\nconst localeProvider = {\n  'en-US': en_US,\n  'ja-JP': ja_JP,\n  'ru-RU': ru_RU,\n  'zh-CN': zh_CN,\n  'zh-TW': zh_TW,\n};\n\nexport { localeProvider };\n"
  },
  {
    "path": "src/common/locales/data/de-DE.json",
    "content": "{\n  \"auth.modal.title\": \"Konto konfigurieren\",\n  \"backend.error.store\": \"Die Erweiterungsgalerie kann nicht skriptgesteuert werden.\",\n  \"backend.imageHosting.baklib.builtInRemark\": \"Baklib integrierter Bildhosting-Dienst.\",\n  \"backend.imageHosting.baklib.name\": \"Baklib\",\n  \"backend.imageHosting.github.form.accessToken\": \"Zugriffstoken\",\n  \"backend.imageHosting.github.form.accessToken.errorMessage\": \"Zugriffstoken ist erforderlich.\",\n  \"backend.imageHosting.github.form.generateNewToken\": \"Neues Token generieren\",\n  \"backend.imageHosting.github.form.repo\": \"Repository\",\n  \"backend.imageHosting.github.form.repo.errorMessage\": \"Bitte wählen Sie ein Repository aus.\",\n  \"backend.imageHosting.github.form.savePath\": \"\",\n  \"backend.imageHosting.github.repo.errorMessage\": \"Bitte konfigurieren Sie das GitHub-Bildhosting erneut.\",\n  \"backend.imageHosting.joplin.builtInRemark\": \"Joplin integrierter Bildhosting-Dienst.\",\n  \"backend.imageHosting.joplin.name\": \"Joplin\",\n  \"backend.imageHosting.leanote.builtInRemark\": \"Leanote integrierter Bildhosting-Dienst.\",\n  \"backend.imageHosting.leanote.name\": \"Leanote\",\n  \"backend.imageHosting.siyuan.builtInRemark\": \"Siyuan Note integrierter Bildhosting-Dienst.\",\n  \"backend.imageHosting.siyuan.name\": \"SiYuan\",\n  \"backend.imageHosting.yuque_oauth.builtInRemark\": \"Yuque integrierter Bildhosting-Dienst.\",\n  \"backend.imageHosting.yuque_oauth.error_401\": \"Keine Berechtigung, das aktuelle Konto löschen und erneut autorisieren.\",\n  \"backend.imageHosting.yuque_oauth.error_403\": \"Keine Berechtigung, das aktuelle Konto löschen und erneut autorisieren.\",\n  \"backend.imageHosting.yuque_oauth.error_429\": \"Zu viele Anfragen. Limit: 100 pro Stunde.\",\n  \"backend.imageHosting.yuque_oauth.name\": \"Yuque Oauth\",\n  \"backend.imageHosting.yuque.name\": \"Yuque\",\n  \"backend.not.unavailable\": \"Das Ausschneiden dieser Art von Seite ist vorübergehend nicht verfügbar.\\n\\nDas Aktualisieren der Seite kann das Problem lösen.\",\n  \"backend.services.baklib.form.authentication\": \"Authentifizierung\",\n  \"backend.services.baklib.headerForm.channel\": \"\",\n  \"backend.services.baklib.headerForm.description\": \"\",\n  \"backend.services.baklib.headerForm.tags\": \"\",\n  \"backend.services.baklib.name\": \"Baklib\",\n  \"backend.services.bear.form.confirm\": \"\",\n  \"backend.services.confluence.form.authentication\": \"\",\n  \"backend.services.confluence.form.origin\": \"\",\n  \"backend.services.confluence.form.space\": \"\",\n  \"backend.services.dida365.headerForm.applyTags\": \"\",\n  \"backend.services.dida365.name\": \"Dida365\",\n  \"backend.services.dida365.rootGroup\": \"\",\n  \"backend.services.dida365.unauthorizedErrorMessage\": \"\",\n  \"backend.services.flomo.login\": \"Nicht autorisiert! Bitte Flomo Web anmelden.\",\n  \"backend.services.github.form.GenerateNewToken\": \"\",\n  \"backend.services.github.form.storageLocation\": \"\",\n  \"backend.services.github.form.storageLocation.code\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePath\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePathPlaceHolder\": \"\",\n  \"backend.services.github.form.storageLocation.issue\": \"\",\n  \"backend.services.github.form.visibility\": \"Sichtbarkeit\",\n  \"backend.services.github.form.visibility.all\": \"Alle\",\n  \"backend.services.github.form.visibility.private\": \"Privat\",\n  \"backend.services.github.form.visibility.public\": \"Öffentlich\",\n  \"backend.services.github.headerForm.applyLabels\": \"\",\n  \"backend.services.joplin.filter_tags\": \"Filter-Tags\",\n  \"backend.services.joplin.filter_unused_tags\": \"Unbenutzte Tags filtern\",\n  \"backend.services.joplin.headerForm.tags\": \"Tags\",\n  \"backend.services.joplin.name\": \"Joplin\",\n  \"backend.services.kindle.form.alert\": \"\",\n  \"backend.services.kindle.name\": \"An Kindle senden\",\n  \"backend.services.leanote.form.email\": \"E-Mail\",\n  \"backend.services.leanote.name\": \"Leanote\",\n  \"backend.services.mail.form.address.is.required\": \"\",\n  \"backend.services.mail.form.buy.powerpack\": \"\",\n  \"backend.services.mail.form.homepage\": \"\",\n  \"backend.services.mail.form.homepage.is.required\": \"\",\n  \"backend.services.mail.form.homepage.of.mail\": \"\",\n  \"backend.services.mail.form.powerpack\": \"\",\n  \"backend.services.mail.form.powerpack.is.expired\": \"\",\n  \"backend.services.mail.form.powerpack.is.required\": \"\",\n  \"backend.services.mail.form.send.html\": \"\",\n  \"backend.services.mail.form.send.html.or.markdown\": \"\",\n  \"backend.services.mail.form.send.to\": \"\",\n  \"backend.services.mail.name\": \"Mail\",\n  \"backend.services.notion.unauthorizedErrorMessage\": \"\",\n  \"backend.services.onenote_oauth.name\": \"\",\n  \"backend.services.server_chan.accessToken.message\": \"\",\n  \"backend.services.server_chan.name\": \"ServerChan\",\n  \"backend.services.siyuan.form.accessToken\": \"Token\",\n  \"backend.services.siyuan.name\": \"SiYuan Note\",\n  \"backend.services.siyuan.notes\": \"Notizen\",\n  \"backend.services.ticktick.headerForm.applyTags\": \"\",\n  \"backend.services.ticktick.name\": \"\",\n  \"backend.services.ticktick.rootGroup\": \"\",\n  \"backend.services.ticktick.unauthorizedErrorMessage\": \"\",\n  \"backend.services.wiznote.form.authentication\": \"\",\n  \"backend.services.wiznote.form.origin\": \"\",\n  \"backend.services.wiznote.name\": \"WizNote\",\n  \"backend.services.wolai.unauthorizedErrorMessage\": \"\",\n  \"backend.services.youdao.name\": \"Youdao\",\n  \"backend.services.youdao.unauthorizedErrorMessage\": \"\",\n  \"backend.services.yuque_oauth.name\": \"Yuque Oauth\",\n  \"backend.services.yuque.form.repositoryType\": \"\",\n  \"backend.services.yuque.form.showAllRepository\": \"\",\n  \"backend.services.yuque.form.showGroupRepository\": \"\",\n  \"backend.services.yuque.form.showSelfRepository\": \"\",\n  \"backend.services.yuque.headerForm.slug\": \"Slug\",\n  \"backend.services.yuque.headerForm.slug_error\": \"Der Slug darf nicht leer sein. Es sind nur Buchstaben, Zahlen, Bindestriche, Unterstriche und Punkte erlaubt. Mindestens drei Zeichen.\",\n  \"backend.services.yuque.name\": \"Yuque\",\n  \"background.not_support_message\": \"\",\n  \"component.accountItem.delete\": \"Löschen\",\n  \"component.accountItem.edit\": \"Bearbeiten\",\n  \"component.imagehostingListItem.delete\": \"Löschen\",\n  \"component.imagehostingListItem.edit\": \"Bearbeiten\",\n  \"component.imagehostingListItem.noDescription\": \"Keine Beschreibung\",\n  \"component.imageHostingSelectOption.noDescription\": \"\",\n  \"component.powerpackForm.expired\": \"\",\n  \"component.powerpackForm.required\": \"\",\n  \"contextMenus.selection.save.description\": \"Auswahl speichern\",\n  \"contextMenus.selection.save.template\": \"Speichern von: [{TITLE}]({URL}) \\n\\n## Inhalt\\n{CONTENT}\\n## Notiz\",\n  \"contextMenus.selection.save.title\": \"Auswahl speichern\",\n  \"extension.link.config.autoRunExclude\": \"AutoRun ausschließen\",\n  \"extension.link.config.template\": \"Vorlage\",\n  \"hooks.useOriginForm.origin.message\": \"\",\n  \"page.complete.close\": \"\",\n  \"page.complete.error\": \"Ein Fehler ist aufgetreten\",\n  \"page.complete.message\": \"Zu {name} gehen\",\n  \"page.complete.share\": \"\",\n  \"page.complete.success\": \"Erfolg\",\n  \"preference.account.add\": \"\",\n  \"preference.accountList.add\": \"\",\n  \"preference.accountList.addAccount\": \"\",\n  \"preference.accountList.confirm\": \"\",\n  \"preference.accountList.defaultRepository\": \"\",\n  \"preference.accountList.editAccount\": \"\",\n  \"preference.accountList.imageHost\": \"\",\n  \"preference.accountList.login\": \"\",\n  \"preference.accountList.type\": \"\",\n  \"preference.accountList.verify\": \"\",\n  \"preference.accountList.verifying\": \"\",\n  \"preference.basic.configLanguage.description\": \"Meine Muttersprache ist Chinesisch. Willkommen, eine Übersetzung auf {GitHub} einzureichen.\",\n  \"preference.basic.configLanguage.title\": \"\",\n  \"preference.basic.iconColor.auto\": \"Automatisch\",\n  \"preference.basic.iconColor.dark\": \"Dunkel\",\n  \"preference.basic.iconColor.description\": \"Icon-Farbe\",\n  \"preference.basic.iconColor.light\": \"Hell\",\n  \"preference.basic.iconColor.title\": \"Icon-Farbe\",\n  \"preference.basic.liveRendering.description\": \"\",\n  \"preference.basic.liveRendering.title\": \"\",\n  \"preference.basic.update.button\": \"\",\n  \"preference.basic.update.description\": \"\",\n  \"preference.basic.update.title\": \"\",\n  \"preference.bind.message\": \"\",\n  \"preference.extensions.automaticOperationIsProhibited\": \"\",\n  \"preference.extensions.CancelSetting\": \"\",\n  \"preference.extensions.clipExtensions\": \"\",\n  \"preference.extensions.clipExtensions.tooltip\": \"Klicken Sie auf den Stern 🌟, um die Standarderweiterung auszuwählen.\",\n  \"preference.extensions.ConfiguredAsDefaultExtension\": \"\",\n  \"preference.extensions.contextMenus\": \"Kontextmenüs\",\n  \"preference.extensions.form.reset\": \"Zurücksetzen\",\n  \"preference.extensions.install\": \"\",\n  \"preference.extensions.no.Description\": \"Keine Beschreibung\",\n  \"preference.extensions.require.powerpack\": \"\",\n  \"preference.extensions.require.update\": \"\",\n  \"preference.extensions.runAutomaticOnSaving\": \"Automatische Ausführung beim Speichern\",\n  \"preference.extensions.toolExtensions\": \"\",\n  \"preference.extensions.Uninstall\": \"Deinstallieren\",\n  \"preference.extensions.update\": \"Update\",\n  \"preference.imageHosting.add\": \"\",\n  \"preference.imageHosting.edit\": \"\",\n  \"preference.imageHosting.remark\": \"\",\n  \"preference.imageHosting.type\": \"\",\n  \"preference.powerpack.activate\": \"\",\n  \"preference.powerpack.expiry\": \"Ablaufdatum\",\n  \"preference.powerpack.failed\": \"Fehler beim Laden von Powerpack-Informationen.\",\n  \"preference.powerpack.feature.coffee\": \"\",\n  \"preference.powerpack.feature.coffee.description\": \"\",\n  \"preference.powerpack.feature.ocr\": \"\",\n  \"preference.powerpack.feature.ocr.description\": \"\",\n  \"preference.powerpack.feature.saveToEmail\": \"\",\n  \"preference.powerpack.feature.saveToEmail.description\": \"\",\n  \"preference.powerpack.feature.sendToKindle\": \"\",\n  \"preference.powerpack.feature.sendToKindle.description\": \"\",\n  \"preference.powerpack.features\": \"\",\n  \"preference.powerpack.free.trial\": \"\",\n  \"preference.powerpack.login.github\": \"Mit Github anmelden\",\n  \"preference.powerpack.login.google\": \"Mit Google anmelden\",\n  \"preference.powerpack.logout\": \"Abmelden\",\n  \"preference.powerpack.reload\": \"Neu laden\",\n  \"preference.powerpack.upgrade\": \"Upgrade\",\n  \"preference.tab.account\": \"Konto\",\n  \"preference.tab.basic\": \"\",\n  \"preference.tab.changelog\": \"Änderungsprotokoll\",\n  \"preference.tab.extensions\": \"\",\n  \"preference.tab.imageHost\": \"\",\n  \"preference.tab.powerpack\": \"\",\n  \"preference.tab.privacy\": \"\",\n  \"tool.clipExtensions\": \"Clip-Erweiterung\",\n  \"tool.repository\": \"Repository\",\n  \"tool.save\": \"Inhalt speichern\",\n  \"tool.saveButton.noRepository\": \"Bitte wählen Sie ein Repository aus.\",\n  \"tool.title\": \"Titel\",\n  \"tool.title.required\": \"Titel ist erforderlich\",\n  \"tool.toolExtensions\": \"Tool-Erweiterung\"\n}\n"
  },
  {
    "path": "src/common/locales/data/de-DE.ts",
    "content": "import { LocaleModel } from '@/common/locales/interface';\nimport messages from './de-DE.json';\n\nconst model: LocaleModel = {\n  name: 'Deutsch',\n  locale: 'de-DE',\n  messages,\n  alias: ['de'],\n};\n\nexport default model;\n"
  },
  {
    "path": "src/common/locales/data/en-US.json",
    "content": "{\n  \"auth.modal.title\": \"Account Config\",\n  \"backend.error.store\": \"The extensions gallery cannot be scripted.\",\n  \"backend.imageHosting.baklib.builtInRemark\": \"Baklib built in image hosting service.\",\n  \"backend.imageHosting.baklib.name\": \"Baklib\",\n  \"backend.imageHosting.github.form.accessToken\": \"AccessToken\",\n  \"backend.imageHosting.github.form.accessToken.errorMessage\": \"AccessToken is required.\",\n  \"backend.imageHosting.github.form.generateNewToken\": \"Generate new token\",\n  \"backend.imageHosting.github.form.repo\": \"Repository\",\n  \"backend.imageHosting.github.form.repo.errorMessage\": \"Please select a repository\",\n  \"backend.imageHosting.github.form.savePath\": \"\",\n  \"backend.imageHosting.github.repo.errorMessage\": \"Please config the github imageHosting again.\",\n  \"backend.imageHosting.joplin.builtInRemark\": \"Joplin built in image hosting service.\",\n  \"backend.imageHosting.joplin.name\": \"Joplin\",\n  \"backend.imageHosting.leanote.builtInRemark\": \"Leanote built in image hosting service.\",\n  \"backend.imageHosting.leanote.name\": \"Leanote\",\n  \"backend.imageHosting.siyuan.builtInRemark\": \"Siyuan Note built in image hosting service.\",\n  \"backend.imageHosting.siyuan.name\": \"SiYuan\",\n  \"backend.imageHosting.yuque_oauth.builtInRemark\": \"Yuque built in image hosting service.\",\n  \"backend.imageHosting.yuque_oauth.error_401\": \"No permission, need to delete the current account and re authorize.\",\n  \"backend.imageHosting.yuque_oauth.error_403\": \"No permission, need to delete the current account and re authorize.\",\n  \"backend.imageHosting.yuque_oauth.error_429\": \"Requests are too frequent.Request limit 100 per hour.\",\n  \"backend.imageHosting.yuque_oauth.name\": \"Yuque Oauth\",\n  \"backend.imageHosting.yuque.name\": \"Yuque\",\n  \"backend.not.unavailable\": \"Clipping of this type of page is temporarily unavailable.\\n\\nRefreshing the page can resolve。\",\n  \"backend.services.baklib.form.authentication\": \"Authentication\",\n  \"backend.services.baklib.headerForm.channel\": \"\",\n  \"backend.services.baklib.headerForm.description\": \"\",\n  \"backend.services.baklib.headerForm.tags\": \"\",\n  \"backend.services.baklib.name\": \"Baklib\",\n  \"backend.services.bear.form.confirm\": \"\",\n  \"backend.services.confluence.form.authentication\": \"\",\n  \"backend.services.confluence.form.origin\": \"\",\n  \"backend.services.confluence.form.space\": \"\",\n  \"backend.services.dida365.headerForm.applyTags\": \"\",\n  \"backend.services.dida365.name\": \"Dida365\",\n  \"backend.services.dida365.rootGroup\": \"\",\n  \"backend.services.dida365.unauthorizedErrorMessage\": \"\",\n  \"backend.services.flomo.login\": \"Unauthorized! Please Login Flomo Web.\",\n  \"backend.services.github.form.GenerateNewToken\": \"\",\n  \"backend.services.github.form.storageLocation\": \"\",\n  \"backend.services.github.form.storageLocation.code\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePath\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePathPlaceHolder\": \"\",\n  \"backend.services.github.form.storageLocation.issue\": \"\",\n  \"backend.services.github.form.visibility\": \"Visibility\",\n  \"backend.services.github.form.visibility.all\": \"All\",\n  \"backend.services.github.form.visibility.private\": \"Private\",\n  \"backend.services.github.form.visibility.public\": \"Public\",\n  \"backend.services.github.headerForm.applyLabels\": \"\",\n  \"backend.services.joplin.filter_tags\": \"Filter tags\",\n  \"backend.services.joplin.filter_unused_tags\": \"Filter unused tags\",\n  \"backend.services.joplin.headerForm.tags\": \"Tags\",\n  \"backend.services.joplin.name\": \"Joplin\",\n  \"backend.services.kindle.form.alert\": \"\",\n  \"backend.services.kindle.name\": \"Send to Kindle\",\n  \"backend.services.leanote.form.email\": \"Email\",\n  \"backend.services.leanote.name\": \"Leanote\",\n  \"backend.services.mail.form.address.is.required\": \"\",\n  \"backend.services.mail.form.buy.powerpack\": \"\",\n  \"backend.services.mail.form.homepage\": \"\",\n  \"backend.services.mail.form.homepage.is.required\": \"\",\n  \"backend.services.mail.form.homepage.of.mail\": \"\",\n  \"backend.services.mail.form.powerpack\": \"\",\n  \"backend.services.mail.form.powerpack.is.expired\": \"\",\n  \"backend.services.mail.form.powerpack.is.required\": \"\",\n  \"backend.services.mail.form.send.html\": \"\",\n  \"backend.services.mail.form.send.html.or.markdown\": \"\",\n  \"backend.services.mail.form.send.to\": \"\",\n  \"backend.services.mail.name\": \"Mail\",\n  \"backend.services.notion.unauthorizedErrorMessage\": \"\",\n  \"backend.services.onenote_oauth.name\": \"\",\n  \"backend.services.server_chan.accessToken.message\": \"\",\n  \"backend.services.server_chan.name\": \"ServerChan\",\n  \"backend.services.siyuan.form.accessToken\": \"Token\",\n  \"backend.services.siyuan.name\": \"SiYuan Note\",\n  \"backend.services.siyuan.notes\": \"Notes\",\n  \"backend.services.ticktick.headerForm.applyTags\": \"\",\n  \"backend.services.ticktick.name\": \"\",\n  \"backend.services.ticktick.rootGroup\": \"\",\n  \"backend.services.ticktick.unauthorizedErrorMessage\": \"\",\n  \"backend.services.wiznote.form.authentication\": \"\",\n  \"backend.services.wiznote.form.origin\": \"\",\n  \"backend.services.wiznote.name\": \"WizNote\",\n  \"backend.services.wolai.unauthorizedErrorMessage\": \"\",\n  \"backend.services.buildin.unauthorizedErrorMessage\": \"Auth failed, Please login build.ai first\",\n  \"backend.services.youdao.name\": \"Youdao\",\n  \"backend.services.youdao.unauthorizedErrorMessage\": \"\",\n  \"backend.services.yuque_oauth.name\": \"Yuque Oauth\",\n  \"backend.services.yuque.form.repositoryType\": \"\",\n  \"backend.services.yuque.form.showAllRepository\": \"\",\n  \"backend.services.yuque.form.showGroupRepository\": \"\",\n  \"backend.services.yuque.form.showSelfRepository\": \"\",\n  \"backend.services.yuque.headerForm.slug\": \"Slug\",\n  \"backend.services.yuque.headerForm.slug_error\": \"The slug cannot be empty. Only letters, numbers, hyphen, underscore and dot are allowed. At least three characters.\",\n  \"backend.services.yuque.name\": \"Yuque\",\n  \"background.not_support_message\": \"\",\n  \"component.accountItem.delete\": \"Delete\",\n  \"component.accountItem.edit\": \"Edit\",\n  \"component.imagehostingListItem.delete\": \"Delete\",\n  \"component.imagehostingListItem.edit\": \"Edit\",\n  \"component.imagehostingListItem.noDescription\": \"No Description\",\n  \"component.imageHostingSelectOption.noDescription\": \"\",\n  \"component.powerpackForm.expired\": \"\",\n  \"component.powerpackForm.required\": \"\",\n  \"contextMenus.selection.save.description\": \"Save selection context\",\n  \"contextMenus.selection.save.template\": \"Save From : [{TITLE}]({URL}) \\n\\n## Content\\n{CONTENT}\\n## Note\",\n  \"contextMenus.selection.save.title\": \"Save selection\",\n  \"extension.link.config.autoRunExclude\": \"AutoRun Exclude\",\n  \"extension.link.config.template\": \"Template\",\n  \"hooks.useOriginForm.origin.message\": \"\",\n  \"page.complete.close\": \"\",\n  \"page.complete.error\": \"Some Error\",\n  \"page.complete.message\": \"Go to {name}\",\n  \"page.complete.share\": \"\",\n  \"page.complete.success\": \"Success\",\n  \"preference.account.add\": \"\",\n  \"preference.accountList.add\": \"\",\n  \"preference.accountList.addAccount\": \"\",\n  \"preference.accountList.confirm\": \"\",\n  \"preference.accountList.defaultRepository\": \"\",\n  \"preference.accountList.editAccount\": \"\",\n  \"preference.accountList.imageHost\": \"\",\n  \"preference.accountList.login\": \"\",\n  \"preference.accountList.type\": \"\",\n  \"preference.accountList.verify\": \"\",\n  \"preference.accountList.verifying\": \"\",\n  \"preference.basic.configLanguage.description\": \"My native language is Chinese,Welcome to submit a translation on {GitHub}.\",\n  \"preference.basic.configLanguage.title\": \"\",\n  \"preference.basic.iconColor.auto\": \"Auto\",\n  \"preference.basic.iconColor.dark\": \"Dark\",\n  \"preference.basic.iconColor.description\": \"Icon Color\",\n  \"preference.basic.iconColor.light\": \"Light\",\n  \"preference.basic.iconColor.title\": \"Icon Color\",\n  \"preference.basic.liveRendering.description\": \"\",\n  \"preference.basic.liveRendering.title\": \"\",\n  \"preference.basic.update.button\": \"\",\n  \"preference.basic.update.description\": \"\",\n  \"preference.basic.update.title\": \"\",\n  \"preference.bind.message\": \"\",\n  \"preference.extensions.automaticOperationIsProhibited\": \"\",\n  \"preference.extensions.CancelSetting\": \"\",\n  \"preference.extensions.clipExtensions\": \"\",\n  \"preference.extensions.clipExtensions.tooltip\": \"Click on the 🌟 to choose the default extension.\",\n  \"preference.extensions.ConfiguredAsDefaultExtension\": \"\",\n  \"preference.extensions.contextMenus\": \"Context Menus\",\n  \"preference.extensions.form.reset\": \"Rest\",\n  \"preference.extensions.install\": \"\",\n  \"preference.extensions.no.Description\": \"No Description\",\n  \"preference.extensions.require.powerpack\": \"\",\n  \"preference.extensions.require.update\": \"\",\n  \"preference.extensions.runAutomaticOnSaving\": \"Run Automatic On Saving\",\n  \"preference.extensions.toolExtensions\": \"\",\n  \"preference.extensions.Uninstall\": \"Uninstall\",\n  \"preference.extensions.update\": \"Update\",\n  \"preference.imageHosting.add\": \"\",\n  \"preference.imageHosting.edit\": \"\",\n  \"preference.imageHosting.remark\": \"\",\n  \"preference.imageHosting.type\": \"\",\n  \"preference.powerpack.activate\": \"\",\n  \"preference.powerpack.expiry\": \"Expiry\",\n  \"preference.powerpack.failed\": \"Failed to load powerpack info.\",\n  \"preference.powerpack.feature.coffee\": \"\",\n  \"preference.powerpack.feature.coffee.description\": \"\",\n  \"preference.powerpack.feature.ocr\": \"\",\n  \"preference.powerpack.feature.ocr.description\": \"\",\n  \"preference.powerpack.feature.saveToEmail\": \"\",\n  \"preference.powerpack.feature.saveToEmail.description\": \"\",\n  \"preference.powerpack.feature.sendToKindle\": \"\",\n  \"preference.powerpack.feature.sendToKindle.description\": \"\",\n  \"preference.powerpack.features\": \"\",\n  \"preference.powerpack.free.trial\": \"\",\n  \"preference.powerpack.login.github\": \"Login with Github\",\n  \"preference.powerpack.login.google\": \"Login with Google\",\n  \"preference.powerpack.logout\": \"Logout\",\n  \"preference.powerpack.reload\": \"Reload\",\n  \"preference.powerpack.upgrade\": \"Upgrade\",\n  \"preference.tab.account\": \"Account\",\n  \"preference.tab.basic\": \"\",\n  \"preference.tab.changelog\": \"Changelog\",\n  \"preference.tab.extensions\": \"\",\n  \"preference.tab.imageHost\": \"\",\n  \"preference.tab.powerpack\": \"\",\n  \"preference.tab.privacy\": \"\",\n  \"tool.clipExtensions\": \"Clip Extension\",\n  \"tool.repository\": \"Repository\",\n  \"tool.save\": \"Save Content\",\n  \"tool.saveButton.noRepository\": \"Please select a repository\",\n  \"tool.title\": \"Title\",\n  \"tool.title.required\": \"Title is Required\",\n  \"tool.toolExtensions\": \"Tool Extension\"\n}\n"
  },
  {
    "path": "src/common/locales/data/en-US.ts",
    "content": "import { LocaleModel } from '@/common/locales/interface';\nimport messages from './en-US.json';\n\nconst model: LocaleModel = {\n  name: 'English',\n  locale: 'en-US',\n  messages,\n  alias: ['en'],\n};\n\nexport default model;\n"
  },
  {
    "path": "src/common/locales/data/ja-JP.json",
    "content": "{\n  \"auth.modal.title\": \"\",\n  \"backend.error.store\": \"\",\n  \"backend.imageHosting.baklib.builtInRemark\": \"\",\n  \"backend.imageHosting.baklib.name\": \"\",\n  \"backend.imageHosting.github.form.accessToken\": \"\",\n  \"backend.imageHosting.github.form.accessToken.errorMessage\": \"\",\n  \"backend.imageHosting.github.form.generateNewToken\": \"\",\n  \"backend.imageHosting.github.form.repo\": \"\",\n  \"backend.imageHosting.github.form.repo.errorMessage\": \"\",\n  \"backend.imageHosting.github.form.savePath\": \"\",\n  \"backend.imageHosting.github.repo.errorMessage\": \"\",\n  \"backend.imageHosting.joplin.builtInRemark\": \"\",\n  \"backend.imageHosting.joplin.name\": \"\",\n  \"backend.imageHosting.leanote.builtInRemark\": \"\",\n  \"backend.imageHosting.leanote.name\": \"\",\n  \"backend.imageHosting.siyuan.builtInRemark\": \"\",\n  \"backend.imageHosting.siyuan.name\": \"\",\n  \"backend.imageHosting.yuque_oauth.builtInRemark\": \"\",\n  \"backend.imageHosting.yuque_oauth.error_401\": \"\",\n  \"backend.imageHosting.yuque_oauth.error_403\": \"\",\n  \"backend.imageHosting.yuque_oauth.error_429\": \"\",\n  \"backend.imageHosting.yuque_oauth.name\": \"\",\n  \"backend.imageHosting.yuque.name\": \"\",\n  \"backend.not.unavailable\": \"\",\n  \"backend.services.baklib.form.authentication\": \"\",\n  \"backend.services.baklib.headerForm.channel\": \"\",\n  \"backend.services.baklib.headerForm.description\": \"\",\n  \"backend.services.baklib.headerForm.tags\": \"\",\n  \"backend.services.baklib.name\": \"\",\n  \"backend.services.bear.form.confirm\": \"\",\n  \"backend.services.confluence.form.authentication\": \"\",\n  \"backend.services.confluence.form.origin\": \"\",\n  \"backend.services.confluence.form.space\": \"\",\n  \"backend.services.dida365.headerForm.applyTags\": \"\",\n  \"backend.services.dida365.name\": \"\",\n  \"backend.services.dida365.rootGroup\": \"\",\n  \"backend.services.dida365.unauthorizedErrorMessage\": \"\",\n  \"backend.services.flomo.login\": \"\",\n  \"backend.services.github.form.GenerateNewToken\": \"\",\n  \"backend.services.github.form.storageLocation\": \"\",\n  \"backend.services.github.form.storageLocation.code\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePath\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePathPlaceHolder\": \"\",\n  \"backend.services.github.form.storageLocation.issue\": \"\",\n  \"backend.services.github.form.visibility\": \"\",\n  \"backend.services.github.form.visibility.all\": \"\",\n  \"backend.services.github.form.visibility.private\": \"\",\n  \"backend.services.github.form.visibility.public\": \"\",\n  \"backend.services.github.headerForm.applyLabels\": \"\",\n  \"backend.services.joplin.filter_tags\": \"\",\n  \"backend.services.joplin.filter_unused_tags\": \"\",\n  \"backend.services.joplin.headerForm.tags\": \"\",\n  \"backend.services.joplin.name\": \"\",\n  \"backend.services.kindle.form.alert\": \"\",\n  \"backend.services.kindle.name\": \"\",\n  \"backend.services.leanote.form.email\": \"\",\n  \"backend.services.leanote.name\": \"\",\n  \"backend.services.mail.form.address.is.required\": \"\",\n  \"backend.services.mail.form.buy.powerpack\": \"\",\n  \"backend.services.mail.form.homepage\": \"\",\n  \"backend.services.mail.form.homepage.is.required\": \"\",\n  \"backend.services.mail.form.homepage.of.mail\": \"\",\n  \"backend.services.mail.form.powerpack\": \"\",\n  \"backend.services.mail.form.powerpack.is.expired\": \"\",\n  \"backend.services.mail.form.powerpack.is.required\": \"\",\n  \"backend.services.mail.form.send.html\": \"\",\n  \"backend.services.mail.form.send.html.or.markdown\": \"\",\n  \"backend.services.mail.form.send.to\": \"\",\n  \"backend.services.mail.name\": \"\",\n  \"backend.services.notion.unauthorizedErrorMessage\": \"\",\n  \"backend.services.onenote_oauth.name\": \"\",\n  \"backend.services.server_chan.accessToken.message\": \"\",\n  \"backend.services.server_chan.name\": \"\",\n  \"backend.services.siyuan.form.accessToken\": \"\",\n  \"backend.services.siyuan.name\": \"\",\n  \"backend.services.siyuan.notes\": \"\",\n  \"backend.services.ticktick.headerForm.applyTags\": \"\",\n  \"backend.services.ticktick.name\": \"\",\n  \"backend.services.ticktick.rootGroup\": \"\",\n  \"backend.services.ticktick.unauthorizedErrorMessage\": \"\",\n  \"backend.services.wiznote.form.authentication\": \"\",\n  \"backend.services.wiznote.form.origin\": \"\",\n  \"backend.services.wiznote.name\": \"\",\n  \"backend.services.wolai.unauthorizedErrorMessage\": \"\",\n  \"backend.services.youdao.name\": \"\",\n  \"backend.services.youdao.unauthorizedErrorMessage\": \"\",\n  \"backend.services.yuque_oauth.name\": \"\",\n  \"backend.services.yuque.form.repositoryType\": \"\",\n  \"backend.services.yuque.form.showAllRepository\": \"\",\n  \"backend.services.yuque.form.showGroupRepository\": \"\",\n  \"backend.services.yuque.form.showSelfRepository\": \"\",\n  \"backend.services.yuque.headerForm.slug\": \"\",\n  \"backend.services.yuque.headerForm.slug_error\": \"\",\n  \"backend.services.yuque.name\": \"\",\n  \"background.not_support_message\": \"\",\n  \"component.accountItem.delete\": \"\",\n  \"component.accountItem.edit\": \"\",\n  \"component.imagehostingListItem.delete\": \"\",\n  \"component.imagehostingListItem.edit\": \"\",\n  \"component.imagehostingListItem.noDescription\": \"\",\n  \"component.imageHostingSelectOption.noDescription\": \"\",\n  \"component.powerpackForm.expired\": \"\",\n  \"component.powerpackForm.required\": \"\",\n  \"contextMenus.selection.save.description\": \"\",\n  \"contextMenus.selection.save.template\": \"\",\n  \"contextMenus.selection.save.title\": \"\",\n  \"extension.link.config.autoRunExclude\": \"\",\n  \"extension.link.config.template\": \"\",\n  \"hooks.useOriginForm.origin.message\": \"\",\n  \"page.complete.close\": \"\",\n  \"page.complete.error\": \"\",\n  \"page.complete.message\": \"\",\n  \"page.complete.share\": \"\",\n  \"page.complete.success\": \"\",\n  \"preference.account.add\": \"\",\n  \"preference.accountList.add\": \"\",\n  \"preference.accountList.addAccount\": \"\",\n  \"preference.accountList.confirm\": \"\",\n  \"preference.accountList.defaultRepository\": \"\",\n  \"preference.accountList.editAccount\": \"\",\n  \"preference.accountList.imageHost\": \"\",\n  \"preference.accountList.login\": \"\",\n  \"preference.accountList.type\": \"\",\n  \"preference.accountList.verify\": \"\",\n  \"preference.accountList.verifying\": \"\",\n  \"preference.basic.configLanguage.description\": \"私の母国語は中国語です,{GitHub}で翻訳を送信してください.\",\n  \"preference.basic.configLanguage.title\": \"\",\n  \"preference.basic.iconColor.auto\": \"\",\n  \"preference.basic.iconColor.dark\": \"\",\n  \"preference.basic.iconColor.description\": \"\",\n  \"preference.basic.iconColor.light\": \"\",\n  \"preference.basic.iconColor.title\": \"\",\n  \"preference.basic.liveRendering.description\": \"\",\n  \"preference.basic.liveRendering.title\": \"\",\n  \"preference.basic.update.button\": \"\",\n  \"preference.basic.update.description\": \"\",\n  \"preference.basic.update.title\": \"\",\n  \"preference.bind.message\": \"\",\n  \"preference.extensions.automaticOperationIsProhibited\": \"\",\n  \"preference.extensions.CancelSetting\": \"\",\n  \"preference.extensions.clipExtensions\": \"\",\n  \"preference.extensions.clipExtensions.tooltip\": \"\",\n  \"preference.extensions.ConfiguredAsDefaultExtension\": \"\",\n  \"preference.extensions.contextMenus\": \"\",\n  \"preference.extensions.form.reset\": \"\",\n  \"preference.extensions.install\": \"\",\n  \"preference.extensions.no.Description\": \"\",\n  \"preference.extensions.require.powerpack\": \"\",\n  \"preference.extensions.require.update\": \"\",\n  \"preference.extensions.runAutomaticOnSaving\": \"\",\n  \"preference.extensions.toolExtensions\": \"\",\n  \"preference.extensions.Uninstall\": \"\",\n  \"preference.extensions.update\": \"\",\n  \"preference.imageHosting.add\": \"\",\n  \"preference.imageHosting.edit\": \"\",\n  \"preference.imageHosting.remark\": \"\",\n  \"preference.imageHosting.type\": \"\",\n  \"preference.powerpack.activate\": \"\",\n  \"preference.powerpack.expiry\": \"\",\n  \"preference.powerpack.failed\": \"\",\n  \"preference.powerpack.feature.coffee\": \"\",\n  \"preference.powerpack.feature.coffee.description\": \"\",\n  \"preference.powerpack.feature.ocr\": \"\",\n  \"preference.powerpack.feature.ocr.description\": \"\",\n  \"preference.powerpack.feature.saveToEmail\": \"\",\n  \"preference.powerpack.feature.saveToEmail.description\": \"\",\n  \"preference.powerpack.feature.sendToKindle\": \"\",\n  \"preference.powerpack.feature.sendToKindle.description\": \"\",\n  \"preference.powerpack.features\": \"\",\n  \"preference.powerpack.free.trial\": \"\",\n  \"preference.powerpack.login.github\": \"\",\n  \"preference.powerpack.login.google\": \"\",\n  \"preference.powerpack.logout\": \"\",\n  \"preference.powerpack.reload\": \"\",\n  \"preference.powerpack.upgrade\": \"\",\n  \"preference.tab.account\": \"\",\n  \"preference.tab.basic\": \"\",\n  \"preference.tab.changelog\": \"\",\n  \"preference.tab.extensions\": \"\",\n  \"preference.tab.imageHost\": \"\",\n  \"preference.tab.powerpack\": \"\",\n  \"preference.tab.privacy\": \"\",\n  \"tool.clipExtensions\": \"\",\n  \"tool.repository\": \"\",\n  \"tool.save\": \"コンテンツを保存する\",\n  \"tool.saveButton.noRepository\": \"\",\n  \"tool.title\": \"記事タイトル\",\n  \"tool.title.required\": \"\",\n  \"tool.toolExtensions\": \"\"\n}"
  },
  {
    "path": "src/common/locales/data/ja-JP.ts",
    "content": "import { LocaleModel } from '@/common/locales/interface';\nimport messages from './ja-JP.json';\n\nconst model: LocaleModel = {\n  name: '日本語',\n  locale: 'ja-JP',\n  messages,\n  alias: ['jp'],\n};\n\nexport default model;\n"
  },
  {
    "path": "src/common/locales/data/ko-KR.json",
    "content": "{\n  \"auth.modal.title\": \"계정 설정\",\n  \"backend.error.store\": \"\",\n  \"backend.imageHosting.baklib.builtInRemark\": \"\",\n  \"backend.imageHosting.baklib.name\": \"Baklib\",\n  \"backend.imageHosting.github.form.accessToken\": \"\",\n  \"backend.imageHosting.github.form.accessToken.errorMessage\": \"\",\n  \"backend.imageHosting.github.form.generateNewToken\": \"\",\n  \"backend.imageHosting.github.form.repo\": \"\",\n  \"backend.imageHosting.github.form.repo.errorMessage\": \"\",\n  \"backend.imageHosting.github.form.savePath\": \"\",\n  \"backend.imageHosting.github.repo.errorMessage\": \"\",\n  \"backend.imageHosting.joplin.builtInRemark\": \"\",\n  \"backend.imageHosting.joplin.name\": \"\",\n  \"backend.imageHosting.leanote.builtInRemark\": \"\",\n  \"backend.imageHosting.leanote.name\": \"\",\n  \"backend.imageHosting.siyuan.builtInRemark\": \"\",\n  \"backend.imageHosting.siyuan.name\": \"\",\n  \"backend.imageHosting.yuque_oauth.builtInRemark\": \"\",\n  \"backend.imageHosting.yuque_oauth.error_401\": \"\",\n  \"backend.imageHosting.yuque_oauth.error_403\": \"\",\n  \"backend.imageHosting.yuque_oauth.error_429\": \"\",\n  \"backend.imageHosting.yuque_oauth.name\": \"\",\n  \"backend.imageHosting.yuque.name\": \"\",\n  \"backend.not.unavailable\": \"\",\n  \"backend.services.baklib.form.authentication\": \"\",\n  \"backend.services.baklib.headerForm.channel\": \"\",\n  \"backend.services.baklib.headerForm.description\": \"\",\n  \"backend.services.baklib.headerForm.tags\": \"\",\n  \"backend.services.baklib.name\": \"\",\n  \"backend.services.bear.form.confirm\": \"\",\n  \"backend.services.confluence.form.authentication\": \"\",\n  \"backend.services.confluence.form.origin\": \"\",\n  \"backend.services.confluence.form.space\": \"\",\n  \"backend.services.dida365.headerForm.applyTags\": \"\",\n  \"backend.services.dida365.name\": \"\",\n  \"backend.services.dida365.rootGroup\": \"\",\n  \"backend.services.dida365.unauthorizedErrorMessage\": \"\",\n  \"backend.services.flomo.login\": \"\",\n  \"backend.services.github.form.GenerateNewToken\": \"\",\n  \"backend.services.github.form.storageLocation\": \"\",\n  \"backend.services.github.form.storageLocation.code\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePath\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePathPlaceHolder\": \"\",\n  \"backend.services.github.form.storageLocation.issue\": \"\",\n  \"backend.services.github.form.visibility\": \"공개범위\",\n  \"backend.services.github.form.visibility.all\": \"전체\",\n  \"backend.services.github.form.visibility.private\": \"프라이빗\",\n  \"backend.services.github.form.visibility.public\": \"퍼블릭\",\n  \"backend.services.github.headerForm.applyLabels\": \"\",\n  \"backend.services.joplin.filter_tags\": \"Filter tags\",\n  \"backend.services.joplin.filter_unused_tags\": \"Filter unused tags\",\n  \"backend.services.joplin.headerForm.tags\": \"Tags\",\n  \"backend.services.joplin.name\": \"Joplin\",\n  \"backend.services.kindle.form.alert\": \"\",\n  \"backend.services.kindle.name\": \"Kindle로 보내기\",\n  \"backend.services.leanote.form.email\": \"이메일\",\n  \"backend.services.leanote.name\": \"Leanote\",\n  \"backend.services.mail.form.address.is.required\": \"\",\n  \"backend.services.mail.form.buy.powerpack\": \"\",\n  \"backend.services.mail.form.homepage\": \"\",\n  \"backend.services.mail.form.homepage.is.required\": \"\",\n  \"backend.services.mail.form.homepage.of.mail\": \"\",\n  \"backend.services.mail.form.powerpack\": \"\",\n  \"backend.services.mail.form.powerpack.is.expired\": \"\",\n  \"backend.services.mail.form.powerpack.is.required\": \"\",\n  \"backend.services.mail.form.send.html\": \"\",\n  \"backend.services.mail.form.send.html.or.markdown\": \"\",\n  \"backend.services.mail.form.send.to\": \"\",\n  \"backend.services.mail.name\": \"메일\",\n  \"backend.services.notion.unauthorizedErrorMessage\": \"\",\n  \"backend.services.onenote_oauth.name\": \"\",\n  \"backend.services.server_chan.accessToken.message\": \"\",\n  \"backend.services.server_chan.name\": \"\",\n  \"backend.services.siyuan.form.accessToken\": \"\",\n  \"backend.services.siyuan.name\": \"\",\n  \"backend.services.siyuan.notes\": \"\",\n  \"backend.services.ticktick.headerForm.applyTags\": \"\",\n  \"backend.services.ticktick.name\": \"\",\n  \"backend.services.ticktick.rootGroup\": \"\",\n  \"backend.services.ticktick.unauthorizedErrorMessage\": \"\",\n  \"backend.services.wiznote.form.authentication\": \"\",\n  \"backend.services.wiznote.form.origin\": \"\",\n  \"backend.services.wiznote.name\": \"\",\n  \"backend.services.wolai.unauthorizedErrorMessage\": \"\",\n  \"backend.services.youdao.name\": \"\",\n  \"backend.services.youdao.unauthorizedErrorMessage\": \"\",\n  \"backend.services.yuque_oauth.name\": \"\",\n  \"backend.services.yuque.form.repositoryType\": \"\",\n  \"backend.services.yuque.form.showAllRepository\": \"\",\n  \"backend.services.yuque.form.showGroupRepository\": \"\",\n  \"backend.services.yuque.form.showSelfRepository\": \"\",\n  \"backend.services.yuque.headerForm.slug\": \"\",\n  \"backend.services.yuque.headerForm.slug_error\": \"\",\n  \"backend.services.yuque.name\": \"\",\n  \"background.not_support_message\": \"\",\n  \"component.accountItem.delete\": \"삭제\",\n  \"component.accountItem.edit\": \"수정\",\n  \"component.imagehostingListItem.delete\": \"삭제\",\n  \"component.imagehostingListItem.edit\": \"수정\",\n  \"component.imagehostingListItem.noDescription\": \"설명 없음\",\n  \"component.imageHostingSelectOption.noDescription\": \"\",\n  \"component.powerpackForm.expired\": \"\",\n  \"component.powerpackForm.required\": \"\",\n  \"contextMenus.selection.save.description\": \"영역의 콘텐츠 저장\",\n  \"contextMenus.selection.save.template\": \"페이지: [{TITLE}]({URL}) \\n\\n## 내용\\n{CONTENT}\\n##\",\n  \"contextMenus.selection.save.title\": \"영역 저장\",\n  \"extension.link.config.autoRunExclude\": \"자동실행 제외\",\n  \"extension.link.config.template\": \"템플릿\",\n  \"hooks.useOriginForm.origin.message\": \"\",\n  \"page.complete.close\": \"\",\n  \"page.complete.error\": \"일부 에러\",\n  \"page.complete.message\": \"{name}으로 이동\",\n  \"page.complete.share\": \"\",\n  \"page.complete.success\": \"성공\",\n  \"preference.account.add\": \"\",\n  \"preference.accountList.add\": \"\",\n  \"preference.accountList.addAccount\": \"\",\n  \"preference.accountList.confirm\": \"\",\n  \"preference.accountList.defaultRepository\": \"\",\n  \"preference.accountList.editAccount\": \"\",\n  \"preference.accountList.imageHost\": \"\",\n  \"preference.accountList.login\": \"\",\n  \"preference.accountList.type\": \"\",\n  \"preference.accountList.verify\": \"\",\n  \"preference.accountList.verifying\": \"\",\n  \"preference.basic.configLanguage.description\": \"제 모국어는 중국어입니다, 번역은 {GitHub}에서 함께하실 수 있습니다.\",\n  \"preference.basic.configLanguage.title\": \"\",\n  \"preference.basic.iconColor.auto\": \"자동\",\n  \"preference.basic.iconColor.dark\": \"다크\",\n  \"preference.basic.iconColor.description\": \"아이콘 색상\",\n  \"preference.basic.iconColor.light\": \"라이트\",\n  \"preference.basic.iconColor.title\": \"아이콘 색상\",\n  \"preference.basic.liveRendering.description\": \"\",\n  \"preference.basic.liveRendering.title\": \"\",\n  \"preference.basic.update.button\": \"\",\n  \"preference.basic.update.description\": \"\",\n  \"preference.basic.update.title\": \"\",\n  \"preference.bind.message\": \"\",\n  \"preference.extensions.automaticOperationIsProhibited\": \"\",\n  \"preference.extensions.CancelSetting\": \"\",\n  \"preference.extensions.clipExtensions\": \"\",\n  \"preference.extensions.clipExtensions.tooltip\": \"🌟를 클릭하여 기본 확장을 선택하세요\",\n  \"preference.extensions.ConfiguredAsDefaultExtension\": \"\",\n  \"preference.extensions.contextMenus\": \"\",\n  \"preference.extensions.form.reset\": \"초기화\",\n  \"preference.extensions.install\": \"\",\n  \"preference.extensions.no.Description\": \"설명 없음\",\n  \"preference.extensions.require.powerpack\": \"\",\n  \"preference.extensions.require.update\": \"\",\n  \"preference.extensions.runAutomaticOnSaving\": \"\",\n  \"preference.extensions.toolExtensions\": \"\",\n  \"preference.extensions.Uninstall\": \"설치 제거\",\n  \"preference.extensions.update\": \"업데이트\",\n  \"preference.imageHosting.add\": \"\",\n  \"preference.imageHosting.edit\": \"\",\n  \"preference.imageHosting.remark\": \"\",\n  \"preference.imageHosting.type\": \"\",\n  \"preference.powerpack.activate\": \"\",\n  \"preference.powerpack.expiry\": \"만료\",\n  \"preference.powerpack.failed\": \"파워팩 정보를 읽어오지 못했습니다\",\n  \"preference.powerpack.feature.coffee\": \"\",\n  \"preference.powerpack.feature.coffee.description\": \"\",\n  \"preference.powerpack.feature.ocr\": \"\",\n  \"preference.powerpack.feature.ocr.description\": \"\",\n  \"preference.powerpack.feature.saveToEmail\": \"\",\n  \"preference.powerpack.feature.saveToEmail.description\": \"\",\n  \"preference.powerpack.feature.sendToKindle\": \"\",\n  \"preference.powerpack.feature.sendToKindle.description\": \"\",\n  \"preference.powerpack.features\": \"\",\n  \"preference.powerpack.free.trial\": \"\",\n  \"preference.powerpack.login.github\": \"Github으로 로그인\",\n  \"preference.powerpack.login.google\": \"Google로 로그인\",\n  \"preference.powerpack.logout\": \"로그아웃\",\n  \"preference.powerpack.reload\": \"새로 고침\",\n  \"preference.powerpack.upgrade\": \"업그레이드\",\n  \"preference.tab.account\": \"계정\",\n  \"preference.tab.basic\": \"\",\n  \"preference.tab.changelog\": \"변경사항\",\n  \"preference.tab.extensions\": \"\",\n  \"preference.tab.imageHost\": \"\",\n  \"preference.tab.powerpack\": \"\",\n  \"preference.tab.privacy\": \"\",\n  \"tool.clipExtensions\": \"클립 확장\",\n  \"tool.repository\": \"저장소\",\n  \"tool.save\": \"컨텐츠 저장\",\n  \"tool.saveButton.noRepository\": \"저장소를 선택하세요\",\n  \"tool.title\": \"컨텐츠 저장\",\n  \"tool.title.required\": \"제목을 입력해주세요\",\n  \"tool.toolExtensions\": \"도구 확장\"\n}\n"
  },
  {
    "path": "src/common/locales/data/ko-KR.ts",
    "content": "import { LocaleModel } from '@/common/locales/interface';\nimport messages from './ko-KR.json';\n\nconst model: LocaleModel = {\n  name: '한국어',\n  locale: 'ko-KR',\n  messages,\n  alias: [],\n};\n\nexport default model;\n"
  },
  {
    "path": "src/common/locales/data/ru-RU.json",
    "content": "{\n  \"auth.modal.title\": \"\",\n  \"backend.error.store\": \"\",\n  \"backend.imageHosting.baklib.builtInRemark\": \"\",\n  \"backend.imageHosting.baklib.name\": \"\",\n  \"backend.imageHosting.github.form.accessToken\": \"\",\n  \"backend.imageHosting.github.form.accessToken.errorMessage\": \"\",\n  \"backend.imageHosting.github.form.generateNewToken\": \"\",\n  \"backend.imageHosting.github.form.repo\": \"\",\n  \"backend.imageHosting.github.form.repo.errorMessage\": \"\",\n  \"backend.imageHosting.github.form.savePath\": \"\",\n  \"backend.imageHosting.github.repo.errorMessage\": \"\",\n  \"backend.imageHosting.joplin.builtInRemark\": \"\",\n  \"backend.imageHosting.joplin.name\": \"\",\n  \"backend.imageHosting.leanote.builtInRemark\": \"\",\n  \"backend.imageHosting.leanote.name\": \"\",\n  \"backend.imageHosting.siyuan.builtInRemark\": \"\",\n  \"backend.imageHosting.siyuan.name\": \"\",\n  \"backend.imageHosting.yuque_oauth.builtInRemark\": \"\",\n  \"backend.imageHosting.yuque_oauth.error_401\": \"\",\n  \"backend.imageHosting.yuque_oauth.error_403\": \"\",\n  \"backend.imageHosting.yuque_oauth.error_429\": \"\",\n  \"backend.imageHosting.yuque_oauth.name\": \"\",\n  \"backend.imageHosting.yuque.name\": \"\",\n  \"backend.not.unavailable\": \"\",\n  \"backend.services.baklib.form.authentication\": \"\",\n  \"backend.services.baklib.headerForm.channel\": \"\",\n  \"backend.services.baklib.headerForm.description\": \"\",\n  \"backend.services.baklib.headerForm.tags\": \"\",\n  \"backend.services.baklib.name\": \"\",\n  \"backend.services.bear.form.confirm\": \"\",\n  \"backend.services.confluence.form.authentication\": \"\",\n  \"backend.services.confluence.form.origin\": \"\",\n  \"backend.services.confluence.form.space\": \"\",\n  \"backend.services.dida365.headerForm.applyTags\": \"\",\n  \"backend.services.dida365.name\": \"\",\n  \"backend.services.dida365.rootGroup\": \"\",\n  \"backend.services.dida365.unauthorizedErrorMessage\": \"\",\n  \"backend.services.flomo.login\": \"\",\n  \"backend.services.github.form.GenerateNewToken\": \"\",\n  \"backend.services.github.form.storageLocation\": \"\",\n  \"backend.services.github.form.storageLocation.code\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePath\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePathPlaceHolder\": \"\",\n  \"backend.services.github.form.storageLocation.issue\": \"\",\n  \"backend.services.github.form.visibility\": \"\",\n  \"backend.services.github.form.visibility.all\": \"\",\n  \"backend.services.github.form.visibility.private\": \"\",\n  \"backend.services.github.form.visibility.public\": \"\",\n  \"backend.services.github.headerForm.applyLabels\": \"\",\n  \"backend.services.joplin.filter_tags\": \"\",\n  \"backend.services.joplin.filter_unused_tags\": \"\",\n  \"backend.services.joplin.headerForm.tags\": \"\",\n  \"backend.services.joplin.name\": \"\",\n  \"backend.services.kindle.form.alert\": \"\",\n  \"backend.services.kindle.name\": \"\",\n  \"backend.services.leanote.form.email\": \"\",\n  \"backend.services.leanote.name\": \"\",\n  \"backend.services.mail.form.address.is.required\": \"\",\n  \"backend.services.mail.form.buy.powerpack\": \"\",\n  \"backend.services.mail.form.homepage\": \"\",\n  \"backend.services.mail.form.homepage.is.required\": \"\",\n  \"backend.services.mail.form.homepage.of.mail\": \"\",\n  \"backend.services.mail.form.powerpack\": \"\",\n  \"backend.services.mail.form.powerpack.is.expired\": \"\",\n  \"backend.services.mail.form.powerpack.is.required\": \"\",\n  \"backend.services.mail.form.send.html\": \"\",\n  \"backend.services.mail.form.send.html.or.markdown\": \"\",\n  \"backend.services.mail.form.send.to\": \"\",\n  \"backend.services.mail.name\": \"\",\n  \"backend.services.notion.unauthorizedErrorMessage\": \"\",\n  \"backend.services.onenote_oauth.name\": \"\",\n  \"backend.services.server_chan.accessToken.message\": \"\",\n  \"backend.services.server_chan.name\": \"\",\n  \"backend.services.siyuan.form.accessToken\": \"\",\n  \"backend.services.siyuan.name\": \"\",\n  \"backend.services.siyuan.notes\": \"\",\n  \"backend.services.ticktick.headerForm.applyTags\": \"\",\n  \"backend.services.ticktick.name\": \"\",\n  \"backend.services.ticktick.rootGroup\": \"\",\n  \"backend.services.ticktick.unauthorizedErrorMessage\": \"\",\n  \"backend.services.wiznote.form.authentication\": \"\",\n  \"backend.services.wiznote.form.origin\": \"\",\n  \"backend.services.wiznote.name\": \"\",\n  \"backend.services.wolai.unauthorizedErrorMessage\": \"\",\n  \"backend.services.youdao.name\": \"\",\n  \"backend.services.youdao.unauthorizedErrorMessage\": \"\",\n  \"backend.services.yuque_oauth.name\": \"\",\n  \"backend.services.yuque.form.repositoryType\": \"\",\n  \"backend.services.yuque.form.showAllRepository\": \"\",\n  \"backend.services.yuque.form.showGroupRepository\": \"\",\n  \"backend.services.yuque.form.showSelfRepository\": \"\",\n  \"backend.services.yuque.headerForm.slug\": \"\",\n  \"backend.services.yuque.headerForm.slug_error\": \"\",\n  \"backend.services.yuque.name\": \"\",\n  \"background.not_support_message\": \"\",\n  \"component.accountItem.delete\": \"\",\n  \"component.accountItem.edit\": \"\",\n  \"component.imagehostingListItem.delete\": \"\",\n  \"component.imagehostingListItem.edit\": \"\",\n  \"component.imagehostingListItem.noDescription\": \"\",\n  \"component.imageHostingSelectOption.noDescription\": \"\",\n  \"component.powerpackForm.expired\": \"\",\n  \"component.powerpackForm.required\": \"\",\n  \"contextMenus.selection.save.description\": \"\",\n  \"contextMenus.selection.save.template\": \"\",\n  \"contextMenus.selection.save.title\": \"\",\n  \"extension.link.config.autoRunExclude\": \"\",\n  \"extension.link.config.template\": \"\",\n  \"hooks.useOriginForm.origin.message\": \"\",\n  \"page.complete.close\": \"\",\n  \"page.complete.error\": \"\",\n  \"page.complete.message\": \"\",\n  \"page.complete.share\": \"\",\n  \"page.complete.success\": \"\",\n  \"preference.account.add\": \"\",\n  \"preference.accountList.add\": \"\",\n  \"preference.accountList.addAccount\": \"\",\n  \"preference.accountList.confirm\": \"\",\n  \"preference.accountList.defaultRepository\": \"\",\n  \"preference.accountList.editAccount\": \"\",\n  \"preference.accountList.imageHost\": \"\",\n  \"preference.accountList.login\": \"\",\n  \"preference.accountList.type\": \"\",\n  \"preference.accountList.verify\": \"\",\n  \"preference.accountList.verifying\": \"\",\n  \"preference.basic.configLanguage.description\": \"Мой родной язык китайский, добро пожаловать на перевод на {GitHub}.\",\n  \"preference.basic.configLanguage.title\": \"\",\n  \"preference.basic.iconColor.auto\": \"\",\n  \"preference.basic.iconColor.dark\": \"\",\n  \"preference.basic.iconColor.description\": \"\",\n  \"preference.basic.iconColor.light\": \"\",\n  \"preference.basic.iconColor.title\": \"\",\n  \"preference.basic.liveRendering.description\": \"\",\n  \"preference.basic.liveRendering.title\": \"\",\n  \"preference.basic.update.button\": \"\",\n  \"preference.basic.update.description\": \"\",\n  \"preference.basic.update.title\": \"\",\n  \"preference.bind.message\": \"\",\n  \"preference.extensions.automaticOperationIsProhibited\": \"\",\n  \"preference.extensions.CancelSetting\": \"\",\n  \"preference.extensions.clipExtensions\": \"\",\n  \"preference.extensions.clipExtensions.tooltip\": \"\",\n  \"preference.extensions.ConfiguredAsDefaultExtension\": \"\",\n  \"preference.extensions.contextMenus\": \"\",\n  \"preference.extensions.form.reset\": \"\",\n  \"preference.extensions.install\": \"\",\n  \"preference.extensions.no.Description\": \"\",\n  \"preference.extensions.require.powerpack\": \"\",\n  \"preference.extensions.require.update\": \"\",\n  \"preference.extensions.runAutomaticOnSaving\": \"\",\n  \"preference.extensions.toolExtensions\": \"\",\n  \"preference.extensions.Uninstall\": \"\",\n  \"preference.extensions.update\": \"\",\n  \"preference.imageHosting.add\": \"\",\n  \"preference.imageHosting.edit\": \"\",\n  \"preference.imageHosting.remark\": \"\",\n  \"preference.imageHosting.type\": \"\",\n  \"preference.powerpack.activate\": \"\",\n  \"preference.powerpack.expiry\": \"\",\n  \"preference.powerpack.failed\": \"\",\n  \"preference.powerpack.feature.coffee\": \"\",\n  \"preference.powerpack.feature.coffee.description\": \"\",\n  \"preference.powerpack.feature.ocr\": \"\",\n  \"preference.powerpack.feature.ocr.description\": \"\",\n  \"preference.powerpack.feature.saveToEmail\": \"\",\n  \"preference.powerpack.feature.saveToEmail.description\": \"\",\n  \"preference.powerpack.feature.sendToKindle\": \"\",\n  \"preference.powerpack.feature.sendToKindle.description\": \"\",\n  \"preference.powerpack.features\": \"\",\n  \"preference.powerpack.free.trial\": \"\",\n  \"preference.powerpack.login.github\": \"\",\n  \"preference.powerpack.login.google\": \"\",\n  \"preference.powerpack.logout\": \"\",\n  \"preference.powerpack.reload\": \"\",\n  \"preference.powerpack.upgrade\": \"\",\n  \"preference.tab.account\": \"\",\n  \"preference.tab.basic\": \"\",\n  \"preference.tab.changelog\": \"\",\n  \"preference.tab.extensions\": \"\",\n  \"preference.tab.imageHost\": \"\",\n  \"preference.tab.powerpack\": \"\",\n  \"preference.tab.privacy\": \"\",\n  \"tool.clipExtensions\": \"\",\n  \"tool.repository\": \"\",\n  \"tool.save\": \"\",\n  \"tool.saveButton.noRepository\": \"\",\n  \"tool.title\": \"Сохранить контент\",\n  \"tool.title.required\": \"\",\n  \"tool.toolExtensions\": \"\"\n}"
  },
  {
    "path": "src/common/locales/data/ru-RU.ts",
    "content": "import { LocaleModel } from '@/common/locales/interface';\nimport messages from './ru-RU.json';\n\nconst model: LocaleModel = {\n  name: 'русский',\n  locale: 'ru-RU',\n  messages,\n  alias: [],\n};\n\nexport default model;\n"
  },
  {
    "path": "src/common/locales/data/zh-CN.json",
    "content": "{\n  \"auth.modal.title\": \"账户配置\",\n  \"backend.error.store\": \"插件商店不允许执行脚本\",\n  \"backend.imageHosting.baklib.builtInRemark\": \"Baklib 内置图床\",\n  \"backend.imageHosting.baklib.name\": \"Baklib\",\n  \"backend.imageHosting.github.form.accessToken\": \"AccessToken\",\n  \"backend.imageHosting.github.form.accessToken.errorMessage\": \"请填写 AccessToken。\",\n  \"backend.imageHosting.github.form.generateNewToken\": \"生成新 Token\",\n  \"backend.imageHosting.github.form.repo\": \"仓库\",\n  \"backend.imageHosting.github.form.repo.errorMessage\": \"请选择仓库。\",\n  \"backend.imageHosting.github.form.savePath\": \"保存路径\",\n  \"backend.imageHosting.github.repo.errorMessage\": \"请重新设置 GitHub 图床\",\n  \"backend.imageHosting.joplin.builtInRemark\": \"Joplin 内置图床\",\n  \"backend.imageHosting.joplin.name\": \"Joplin\",\n  \"backend.imageHosting.leanote.builtInRemark\": \"蚂蚁笔记内置图床\",\n  \"backend.imageHosting.leanote.name\": \"蚂蚁笔记\",\n  \"backend.imageHosting.siyuan.builtInRemark\": \"\",\n  \"backend.imageHosting.siyuan.name\": \"思源笔记\",\n  \"backend.imageHosting.yuque_oauth.builtInRemark\": \"语雀内置图床。\",\n  \"backend.imageHosting.yuque_oauth.error_401\": \"没有权限，需要删除当前账户重新授权。\",\n  \"backend.imageHosting.yuque_oauth.error_403\": \"没有权限，需要删除当前账户重新授权。\",\n  \"backend.imageHosting.yuque_oauth.error_429\": \"请求太频繁，接口限制每小时最多 100 次。\",\n  \"backend.imageHosting.yuque_oauth.name\": \"语雀(一键授权)\",\n  \"backend.imageHosting.yuque.name\": \"语雀\",\n  \"backend.imageHosting.wiznote.name\": \"为知笔记\",\n  \"backend.imageHosting.wiznote.builtInRemark\": \"为知笔记内置图床\",\n  \"backend.not.unavailable\": \"暂时无法剪辑此类型的页面。\\n\\n刷新页面可以解决。\",\n\n  \"backend.services.memos.name\": \"Memos\",\n\t\"backend.services.memos.form.hostTest\": \"检验\",\n  \"backend.services.memos.accessToken.message\": \"请输入 AccessToken\",\n  \"backend.services.memos.form.authentication\": \"请输入服务器地址\",\n\t\"backend.services.memos.headerForm.tag\": \"请输入标签名称，多个标签用英文逗号分隔，如 tag1,tag2...\",\n\t\"backend.services.memos.headerForm.visibility\": \"文档类型\",\n\t\"backend.services.memos.headerForm.VisibilityType.private\": \"私人\",\n\t\"backend.services.memos.headerForm.VisibilityType.public\": \"公开\",\n\t\"backend.services.memos.headerForm.tag_error\": \"标签格式错误，请检查\",\n\n\t\"backend.services.baklib.form.hostTest\": \"测试\",\n  \"backend.services.baklib.form.authentication\": \"授权\",\n  \"backend.services.baklib.headerForm.channel\": \"栏目\",\n  \"backend.services.baklib.headerForm.description\": \"描述\",\n  \"backend.services.baklib.headerForm.tags\": \"标签\",\n  \"backend.services.baklib.name\": \"Baklib\",\n  \"backend.services.bear.form.confirm\": \"请确认您安装了 Bear 客户端。\",\n  \"backend.services.confluence.form.authentication\": \"授权\",\n  \"backend.services.confluence.form.origin\": \"Origin\",\n  \"backend.services.confluence.form.space\": \"空间\",\n  \"backend.services.dida365.headerForm.applyTags\": \"选择标签\",\n  \"backend.services.dida365.name\": \"滴答清单\",\n  \"backend.services.dida365.rootGroup\": \"根目录\",\n  \"backend.services.dida365.unauthorizedErrorMessage\": \"授权失败，请登录网页版滴答清单。\",\n  \"backend.services.flomo.login\": \"授权失败，请登录网页版浮墨笔记。\",\n  \"backend.services.github.form.GenerateNewToken\": \"生成新 Token\",\n  \"backend.services.github.form.storageLocation\": \"保存位置\",\n  \"backend.services.github.form.storageLocation.code\": \"Code\",\n  \"backend.services.github.form.storageLocation.code.savePath\": \"保存路径\",\n  \"backend.services.github.form.storageLocation.code.savePathPlaceHolder\": \"仅在保存到Code时生效\",\n  \"backend.services.github.form.storageLocation.issue\": \"Issue\",\n  \"backend.services.github.form.visibility\": \"可见性\",\n  \"backend.services.github.form.visibility.all\": \"全部\",\n  \"backend.services.github.form.visibility.private\": \"私有仓库\",\n  \"backend.services.github.form.visibility.public\": \"公开仓库\",\n  \"backend.services.github.headerForm.applyLabels\": \"设置标签\",\n  \"backend.services.joplin.filter_tags\": \"过滤标签\",\n  \"backend.services.joplin.filter_unused_tags\": \"过滤没有被使用的标签\",\n  \"backend.services.joplin.headerForm.tags\": \"标签\",\n  \"backend.services.joplin.name\": \"Joplin\",\n  \"backend.services.kindle.form.alert\": \"你必须告诉 Amazon 允许 {mail} 发送邮件到你的 Kindle.\",\n  \"backend.services.kindle.name\": \"发送到 Kindle\",\n  \"backend.services.leanote.form.email\": \"邮箱\",\n  \"backend.services.leanote.name\": \"蚂蚁笔记\",\n  \"backend.services.mail.form.address.is.required\": \"请填写邮件地址\",\n  \"backend.services.mail.form.buy.powerpack\": \"购买加强包\",\n  \"backend.services.mail.form.homepage\": \"邮箱首页\",\n  \"backend.services.mail.form.homepage.is.required\": \"请填写首页地址\",\n  \"backend.services.mail.form.homepage.of.mail\": \"邮箱首页\",\n  \"backend.services.mail.form.powerpack\": \"加强包\",\n  \"backend.services.mail.form.powerpack.is.expired\": \"加强包已过期\",\n  \"backend.services.mail.form.powerpack.is.required\": \"需要购买加强包\",\n  \"backend.services.mail.form.send.html\": \"发送 Html\",\n  \"backend.services.mail.form.send.html.or.markdown\": \"发送 Html 或 Markdown\",\n  \"backend.services.mail.form.send.to\": \"发送给\",\n  \"backend.services.mail.name\": \"邮件\",\n  \"backend.services.notion.unauthorizedErrorMessage\": \"授权失败，请登录网页版 Notion。\",\n  \"backend.services.onenote_oauth.name\": \"OneNote\",\n  \"backend.services.qcloud.name\": \"腾讯云\",\n  \"backend.services.server_chan.accessToken.message\": \"请输入 AccessToken\",\n  \"backend.services.server_chan.name\": \"Server酱\",\n  \"backend.services.siyuan.form.accessToken\": \"\",\n  \"backend.services.siyuan.name\": \"思源笔记\",\n  \"backend.services.siyuan.notes\": \"笔记本\",\n  \"backend.services.ticktick.headerForm.applyTags\": \"选择标签\",\n  \"backend.services.ticktick.name\": \"TickTick\",\n  \"backend.services.ticktick.rootGroup\": \"根目录\",\n  \"backend.services.ticktick.unauthorizedErrorMessage\": \"授权失败，请登录网页版 TickTick。\",\n  \"backend.services.wiznote.form.authentication\": \"授权\",\n  \"backend.services.wiznote.form.origin\": \"Origin\",\n  \"backend.services.wiznote.name\": \"为知笔记\",\n  \"backend.services.wolai.unauthorizedErrorMessage\": \"授权失败，请登录网页版 Wolai\",\n  \"backend.services.flowus.unauthorizedErrorMessage\": \"授权失败，请登录网页版 FlowUs\",\n  \"backend.services.buildin.unauthorizedErrorMessage\": \"授权失败，请登录网页版 Buildin.AI\",\n  \"backend.services.youdao.name\": \"有道云笔记\",\n  \"backend.services.youdao.unauthorizedErrorMessage\": \"授权失败，请登录网页版有道云笔记。\",\n  \"backend.services.yuque_oauth.name\": \"语雀(一键授权)\",\n  \"backend.services.yuque.form.repositoryType\": \"知识库类型\",\n  \"backend.services.yuque.form.showAllRepository\": \"显示全部知识库\",\n  \"backend.services.yuque.form.showGroupRepository\": \"显示团队的知识库\",\n  \"backend.services.yuque.form.showSelfRepository\": \"显示自己的知识库\",\n  \"backend.services.yuque.headerForm.slug\": \"路径\",\n  \"backend.services.yuque.headerForm.slug_error\": \"只能输入大小写字母、横线、下划线和点，至少 2 个字符。\",\n  \"backend.services.yuque.name\": \"语雀\",\n  \"background.not_support_message\": \"暂时无法剪辑此类型的页面。\",\n  \"component.accountItem.delete\": \"删除\",\n  \"component.accountItem.edit\": \"编辑\",\n  \"component.imagehostingListItem.delete\": \"删除\",\n  \"component.imagehostingListItem.edit\": \"编辑\",\n  \"component.imagehostingListItem.noDescription\": \"没有备注\",\n  \"component.imageHostingSelectOption.noDescription\": \"没有备注\",\n  \"component.powerpackForm.expired\": \"加强包已过期\",\n  \"component.powerpackForm.required\": \"请购买加强包\",\n  \"contextMenus.selection.save.description\": \"保存选择的内容\",\n  \"contextMenus.selection.save.template\": \"来源：[{TITLE}]({URL})\\n\\n## 摘录内容 \\n{CONTENT}\\n## 想法\",\n  \"contextMenus.selection.save.title\": \"保存选择的内容\",\n  \"extension.link.config.autoRunExclude\": \"禁用自动运行\",\n  \"extension.link.config.template\": \"模板\",\n  \"hooks.useOriginForm.origin.message\": \"格式错误,示例 https://developer.mozilla.org\",\n  \"page.complete.close\": \"关闭 Web Clipper\",\n  \"page.complete.error\": \"发生错误\",\n  \"page.complete.message\": \"前往 {name} 查看\",\n  \"page.complete.share\": \"分享\",\n  \"page.complete.success\": \"保存成功\",\n  \"preference.account.add\": \"绑定账户\",\n  \"preference.accountList.add\": \"添加\",\n  \"preference.accountList.addAccount\": \"添加账户\",\n  \"preference.accountList.confirm\": \"确认\",\n  \"preference.accountList.defaultRepository\": \"默认知识库\",\n  \"preference.accountList.editAccount\": \"编辑账户\",\n  \"preference.accountList.imageHost\": \"图床\",\n  \"preference.accountList.login\": \"授权登录\",\n  \"preference.accountList.type\": \"类型\",\n  \"preference.accountList.verify\": \"校验\",\n  \"preference.accountList.verifying\": \"校验中\",\n  \"preference.basic.configLanguage.description\": \"欢迎在 {GitHub} 提交翻译\",\n  \"preference.basic.configLanguage.title\": \"语言\",\n  \"preference.basic.iconColor.auto\": \"自动\",\n  \"preference.basic.iconColor.dark\": \"暗色\",\n  \"preference.basic.iconColor.description\": \"图标颜色\",\n  \"preference.basic.iconColor.light\": \"亮色\",\n  \"preference.basic.iconColor.title\": \"图标颜色\",\n  \"preference.basic.liveRendering.description\": \"开启后编辑器使用所见即所得模式\",\n  \"preference.basic.liveRendering.title\": \"所见即所得\",\n  \"preference.basic.update.button\": \"安装更新\",\n  \"preference.basic.update.description\": \"因为审核需要一周，所以 chrome 商店的版本会延迟几个版本。\",\n  \"preference.basic.update.title\": \"有更新\",\n  \"preference.bind.message\": \"只有绑定账户后才能使用本插件\",\n  \"preference.extensions.automaticOperationIsProhibited\": \"自动运行被禁止\",\n  \"preference.extensions.CancelSetting\": \"取消设置\",\n  \"preference.extensions.clipExtensions\": \"剪藏插件\",\n  \"preference.extensions.clipExtensions.tooltip\": \"点击 🌟选择默认插件\",\n  \"preference.extensions.ConfiguredAsDefaultExtension\": \"设置成默认扩展\",\n  \"preference.extensions.contextMenus\": \"右键菜单\",\n  \"preference.extensions.form.reset\": \"恢复默认设置\",\n  \"preference.extensions.install\": \"安装\",\n  \"preference.extensions.no.Description\": \"没有描述\",\n  \"preference.extensions.require.powerpack\": \"请购买加强包\",\n  \"preference.extensions.require.update\": \"需要剪藏更新到 {version} 版本\",\n  \"preference.extensions.runAutomaticOnSaving\": \"保存时自动运行\",\n  \"preference.extensions.toolExtensions\": \"工具插件\",\n  \"preference.extensions.Uninstall\": \"卸载\",\n  \"preference.extensions.update\": \"更新\",\n  \"preference.imageHosting.add\": \"添加图床\",\n  \"preference.imageHosting.edit\": \"编辑\",\n  \"preference.imageHosting.remark\": \"备注\",\n  \"preference.imageHosting.type\": \"类型\",\n  \"preference.powerpack.activate\": \"购买加强包解锁更多功能\",\n  \"preference.powerpack.expiry\": \"过期时间\",\n  \"preference.powerpack.failed\": \"获取加强包信息失败\",\n  \"preference.powerpack.feature.coffee\": \"给我买杯咖啡\",\n  \"preference.powerpack.feature.coffee.description\": \"让开发者更有维护的动力\",\n  \"preference.powerpack.feature.ocr\": \"OCR\",\n  \"preference.powerpack.feature.ocr.description\": \"识别图片中的文字\",\n  \"preference.powerpack.feature.saveToEmail\": \"保存到邮箱\",\n  \"preference.powerpack.feature.saveToEmail.description\": \"保存网页到邮箱中查看\",\n  \"preference.powerpack.feature.sendToKindle\": \"保存到 Kindle\",\n  \"preference.powerpack.feature.sendToKindle.description\": \"保存网页到 Kindle 中查看\",\n  \"preference.powerpack.features\": \"功能\",\n  \"preference.powerpack.free.trial\": \"免费试用7天\",\n  \"preference.powerpack.login.github\": \"通过 Github 登录\",\n  \"preference.powerpack.login.google\": \"通过 Google 登录\",\n  \"preference.powerpack.logout\": \"登出\",\n  \"preference.powerpack.reload\": \"刷新\",\n  \"preference.powerpack.upgrade\": \"购买\",\n  \"preference.tab.account\": \"账户\",\n  \"preference.tab.basic\": \"基础设置\",\n  \"preference.tab.changelog\": \"更新日志\",\n  \"preference.tab.extensions\": \"扩展设置\",\n  \"preference.tab.imageHost\": \"图床设置\",\n  \"preference.tab.powerpack\": \"加强包\",\n  \"preference.tab.privacy\": \"隐私协议\",\n  \"tool.clipExtensions\": \"剪藏扩展\",\n  \"tool.repository\": \"知识库\",\n  \"tool.save\": \"保存内容\",\n  \"tool.saveButton.noRepository\": \"请选择你要保存的知识库\",\n  \"tool.title\": \"笔记标题\",\n  \"tool.title.required\": \"请输入笔记标题\",\n  \"tool.toolExtensions\": \"工具扩展\"\n}\n"
  },
  {
    "path": "src/common/locales/data/zh-CN.ts",
    "content": "import { LocaleModel } from '@/common/locales/interface';\nimport messages from './zh-CN.json';\n\nconst model: LocaleModel = {\n  name: '简体中文',\n  locale: 'zh-CN',\n  messages,\n  alias: ['zh'],\n};\n\nexport default model;\n"
  },
  {
    "path": "src/common/locales/data/zh-TW.json",
    "content": "{\n  \"auth.modal.title\": \"賬戶配置\",\n  \"backend.error.store\": \"\",\n  \"backend.imageHosting.baklib.builtInRemark\": \"\",\n  \"backend.imageHosting.baklib.name\": \"\",\n  \"backend.imageHosting.github.form.accessToken\": \"\",\n  \"backend.imageHosting.github.form.accessToken.errorMessage\": \"\",\n  \"backend.imageHosting.github.form.generateNewToken\": \"\",\n  \"backend.imageHosting.github.form.repo\": \"\",\n  \"backend.imageHosting.github.form.repo.errorMessage\": \"\",\n  \"backend.imageHosting.github.form.savePath\": \"\",\n  \"backend.imageHosting.github.repo.errorMessage\": \"\",\n  \"backend.imageHosting.joplin.builtInRemark\": \"Joplin 內置圖床\",\n  \"backend.imageHosting.joplin.name\": \"Joplin\",\n  \"backend.imageHosting.leanote.builtInRemark\": \"\",\n  \"backend.imageHosting.leanote.name\": \"\",\n  \"backend.imageHosting.siyuan.builtInRemark\": \"\",\n  \"backend.imageHosting.siyuan.name\": \"\",\n  \"backend.imageHosting.yuque_oauth.builtInRemark\": \"語雀內置圖床。\",\n  \"backend.imageHosting.yuque_oauth.error_401\": \"沒有權限，需要刪除當前賬戶重新授權。\",\n  \"backend.imageHosting.yuque_oauth.error_403\": \"沒有權限，需要刪除當前賬戶重新授權。\",\n  \"backend.imageHosting.yuque_oauth.error_429\": \"請求太頻繁，接口限制每小時最多 100 次。\",\n  \"backend.imageHosting.yuque_oauth.name\": \"語雀(一鍵授權)\",\n  \"backend.imageHosting.yuque.name\": \"語雀\",\n  \"backend.not.unavailable\": \"\",\n  \"backend.services.baklib.form.authentication\": \"\",\n  \"backend.services.baklib.headerForm.channel\": \"欄目\",\n  \"backend.services.baklib.headerForm.description\": \"描述\",\n  \"backend.services.baklib.headerForm.tags\": \"標簽\",\n  \"backend.services.baklib.name\": \"Baklib\",\n  \"backend.services.bear.form.confirm\": \"請確認您安裝了 Bear 客戶端。\",\n  \"backend.services.confluence.form.authentication\": \"授權\",\n  \"backend.services.confluence.form.origin\": \"Origin\",\n  \"backend.services.confluence.form.space\": \"空間\",\n  \"backend.services.dida365.headerForm.applyTags\": \"選擇標簽\",\n  \"backend.services.dida365.name\": \"滴答清單\",\n  \"backend.services.dida365.rootGroup\": \"根目錄\",\n  \"backend.services.dida365.unauthorizedErrorMessage\": \"授權失敗，請登錄網頁版滴答清單。\",\n  \"backend.services.flomo.login\": \"\",\n  \"backend.services.github.form.GenerateNewToken\": \"生成新 Token\",\n  \"backend.services.github.form.storageLocation\": \"\",\n  \"backend.services.github.form.storageLocation.code\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePath\": \"\",\n  \"backend.services.github.form.storageLocation.code.savePathPlaceHolder\": \"\",\n  \"backend.services.github.form.storageLocation.issue\": \"\",\n  \"backend.services.github.form.visibility\": \"可見性\",\n  \"backend.services.github.form.visibility.all\": \"全部\",\n  \"backend.services.github.form.visibility.private\": \"私有倉庫\",\n  \"backend.services.github.form.visibility.public\": \"公開倉庫\",\n  \"backend.services.github.headerForm.applyLabels\": \"設置標簽\",\n  \"backend.services.joplin.filter_tags\": \"過濾標簽\",\n  \"backend.services.joplin.filter_unused_tags\": \"過濾沒有被使用的標簽\",\n  \"backend.services.joplin.headerForm.tags\": \"標簽\",\n  \"backend.services.joplin.name\": \"Joplin\",\n  \"backend.services.kindle.form.alert\": \"你必須告訴 Amazon 允許 {mail} 发送郵件到你的 Kindle.\",\n  \"backend.services.kindle.name\": \"发送到 Kindle\",\n  \"backend.services.leanote.form.email\": \"\",\n  \"backend.services.leanote.name\": \"\",\n  \"backend.services.mail.form.address.is.required\": \"請填寫郵件地址\",\n  \"backend.services.mail.form.buy.powerpack\": \"購買加強包\",\n  \"backend.services.mail.form.homepage\": \"郵箱首頁\",\n  \"backend.services.mail.form.homepage.is.required\": \"請填寫首頁地址\",\n  \"backend.services.mail.form.homepage.of.mail\": \"郵箱首頁\",\n  \"backend.services.mail.form.powerpack\": \"加強包\",\n  \"backend.services.mail.form.powerpack.is.expired\": \"加強包已過期\",\n  \"backend.services.mail.form.powerpack.is.required\": \"需要購買加強包\",\n  \"backend.services.mail.form.send.html\": \"发送 Html\",\n  \"backend.services.mail.form.send.html.or.markdown\": \"发送 Html 或 Markdown\",\n  \"backend.services.mail.form.send.to\": \"发送給\",\n  \"backend.services.mail.name\": \"郵件\",\n  \"backend.services.notion.unauthorizedErrorMessage\": \"授權失敗，請登錄網頁版 Notion。\",\n  \"backend.services.onenote_oauth.name\": \"OneNote\",\n  \"backend.services.qcloud.name\": \"騰訊雲\",\n  \"backend.services.server_chan.accessToken.message\": \"請輸入 AccessToken\",\n  \"backend.services.server_chan.name\": \"Server醬\",\n  \"backend.services.siyuan.form.accessToken\": \"\",\n  \"backend.services.siyuan.name\": \"\",\n  \"backend.services.siyuan.notes\": \"\",\n  \"backend.services.ticktick.headerForm.applyTags\": \"選擇標簽\",\n  \"backend.services.ticktick.name\": \"TickTick\",\n  \"backend.services.ticktick.rootGroup\": \"根目錄\",\n  \"backend.services.ticktick.unauthorizedErrorMessage\": \"授權失敗，請登錄網頁版 TickTick。\",\n  \"backend.services.wiznote.form.authentication\": \"授權\",\n  \"backend.services.wiznote.form.origin\": \"Origin\",\n  \"backend.services.wiznote.name\": \"為知筆記\",\n  \"backend.services.wolai.unauthorizedErrorMessage\": \"授權失敗，請登錄網頁版 Wolai\",\n  \"backend.services.flowus.unauthorizedErrorMessage\": \"授權失敗，請登錄網頁版 FlowUs\",\n  \"backend.services.buildin.unauthorizedErrorMessage\": \"授權失敗，請登錄網頁版 Buildin.AI\",\n  \"backend.services.youdao.name\": \"有道雲筆記\",\n  \"backend.services.youdao.unauthorizedErrorMessage\": \"授權失敗，請登錄網頁版有道雲筆記。\",\n  \"backend.services.yuque_oauth.name\": \"語雀(一鍵授權)\",\n  \"backend.services.yuque.form.repositoryType\": \"知識庫類型\",\n  \"backend.services.yuque.form.showAllRepository\": \"顯示全部知識庫\",\n  \"backend.services.yuque.form.showGroupRepository\": \"顯示團隊的知識庫\",\n  \"backend.services.yuque.form.showSelfRepository\": \"顯示自己的知識庫\",\n  \"backend.services.yuque.headerForm.slug\": \"路徑\",\n  \"backend.services.yuque.headerForm.slug_error\": \"只能輸入大小寫字母、橫線、下劃線和點，至少 2 個字符。\",\n  \"backend.services.yuque.name\": \"語雀\",\n  \"background.not_support_message\": \"暫時無法剪輯此類型的頁面。\",\n  \"component.accountItem.delete\": \"刪除\",\n  \"component.accountItem.edit\": \"編輯\",\n  \"component.imagehostingListItem.delete\": \"刪除\",\n  \"component.imagehostingListItem.edit\": \"編輯\",\n  \"component.imagehostingListItem.noDescription\": \"沒有備注\",\n  \"component.imageHostingSelectOption.noDescription\": \"沒有備注\",\n  \"component.powerpackForm.expired\": \"加強包已過期\",\n  \"component.powerpackForm.required\": \"請購買加強包\",\n  \"contextMenus.selection.save.description\": \"\",\n  \"contextMenus.selection.save.template\": \"\",\n  \"contextMenus.selection.save.title\": \"\",\n  \"extension.link.config.autoRunExclude\": \"\",\n  \"extension.link.config.template\": \"\",\n  \"hooks.useOriginForm.origin.message\": \"格式錯誤,示例 https://developer.mozilla.org\",\n  \"page.complete.close\": \"關閉 Web Clipper\",\n  \"page.complete.error\": \"发生錯誤\",\n  \"page.complete.message\": \"前往 {name} 查看\",\n  \"page.complete.share\": \"分享\",\n  \"page.complete.success\": \"保存成功\",\n  \"preference.account.add\": \"綁定賬戶\",\n  \"preference.accountList.add\": \"添加\",\n  \"preference.accountList.addAccount\": \"添加賬戶\",\n  \"preference.accountList.confirm\": \"確認\",\n  \"preference.accountList.defaultRepository\": \"默認知識庫\",\n  \"preference.accountList.editAccount\": \"編輯賬戶\",\n  \"preference.accountList.imageHost\": \"圖床\",\n  \"preference.accountList.login\": \"授權登錄\",\n  \"preference.accountList.type\": \"類型\",\n  \"preference.accountList.verify\": \"校驗\",\n  \"preference.accountList.verifying\": \"校驗中\",\n  \"preference.basic.configLanguage.description\": \"歡迎在 {GitHub} 提交翻譯\",\n  \"preference.basic.configLanguage.title\": \"語言\",\n  \"preference.basic.iconColor.auto\": \"\",\n  \"preference.basic.iconColor.dark\": \"\",\n  \"preference.basic.iconColor.description\": \"\",\n  \"preference.basic.iconColor.light\": \"\",\n  \"preference.basic.iconColor.title\": \"\",\n  \"preference.basic.liveRendering.description\": \"開啟後編輯器使用所見即所得模式\",\n  \"preference.basic.liveRendering.title\": \"所見即所得\",\n  \"preference.basic.update.button\": \"安裝更新\",\n  \"preference.basic.update.description\": \"因為審核需要一周，所以 chrome 商店的版本會延遲幾個版本。\",\n  \"preference.basic.update.title\": \"有更新\",\n  \"preference.bind.message\": \"只有綁定賬戶後才能使用本插件\",\n  \"preference.extensions.automaticOperationIsProhibited\": \"自動運行被禁止\",\n  \"preference.extensions.CancelSetting\": \"取消設置\",\n  \"preference.extensions.clipExtensions\": \"剪藏插件\",\n  \"preference.extensions.clipExtensions.tooltip\": \"點擊 🌟選擇默認插件\",\n  \"preference.extensions.ConfiguredAsDefaultExtension\": \"設置成默認擴展\",\n  \"preference.extensions.contextMenus\": \"\",\n  \"preference.extensions.form.reset\": \"恢复默认设置\",\n  \"preference.extensions.install\": \"安裝\",\n  \"preference.extensions.no.Description\": \"沒有描述\",\n  \"preference.extensions.require.powerpack\": \"請購買加強包\",\n  \"preference.extensions.require.update\": \"需要剪藏更新到 {version} 版本\",\n  \"preference.extensions.runAutomaticOnSaving\": \"保存時自動運行\",\n  \"preference.extensions.toolExtensions\": \"工具插件\",\n  \"preference.extensions.Uninstall\": \"卸載\",\n  \"preference.extensions.update\": \"更新\",\n  \"preference.imageHosting.add\": \"添加圖床\",\n  \"preference.imageHosting.edit\": \"編輯\",\n  \"preference.imageHosting.remark\": \"備注\",\n  \"preference.imageHosting.type\": \"類型\",\n  \"preference.powerpack.activate\": \"購買加強包解鎖更多功能\",\n  \"preference.powerpack.expiry\": \"過期時間\",\n  \"preference.powerpack.failed\": \"獲取加強包信息失敗\",\n  \"preference.powerpack.feature.coffee\": \"給我買杯咖啡\",\n  \"preference.powerpack.feature.coffee.description\": \"讓開发者更有維護的動力\",\n  \"preference.powerpack.feature.ocr\": \"OCR\",\n  \"preference.powerpack.feature.ocr.description\": \"識別圖片中的文字\",\n  \"preference.powerpack.feature.saveToEmail\": \"保存到郵箱\",\n  \"preference.powerpack.feature.saveToEmail.description\": \"保存網頁到郵箱中查看\",\n  \"preference.powerpack.feature.sendToKindle\": \"保存到 Kindle\",\n  \"preference.powerpack.feature.sendToKindle.description\": \"保存網頁到 Kindle 中查看\",\n  \"preference.powerpack.features\": \"功能\",\n  \"preference.powerpack.free.trial\": \"免費試用7天\",\n  \"preference.powerpack.login.github\": \"通過 Github 登錄\",\n  \"preference.powerpack.login.google\": \"通過 Google 登錄\",\n  \"preference.powerpack.logout\": \"登出\",\n  \"preference.powerpack.reload\": \"刷新\",\n  \"preference.powerpack.upgrade\": \"購買\",\n  \"preference.tab.account\": \"賬戶\",\n  \"preference.tab.basic\": \"基礎設置\",\n  \"preference.tab.changelog\": \"更新日志\",\n  \"preference.tab.extensions\": \"擴展設置\",\n  \"preference.tab.imageHost\": \"圖床設置\",\n  \"preference.tab.powerpack\": \"加強包\",\n  \"preference.tab.privacy\": \"隱私協議\",\n  \"tool.clipExtensions\": \"剪藏擴展\",\n  \"tool.repository\": \"知識庫\",\n  \"tool.save\": \"保存內容\",\n  \"tool.saveButton.noRepository\": \"\",\n  \"tool.title\": \"筆記標題\",\n  \"tool.title.required\": \"請輸入筆記標題\",\n  \"tool.toolExtensions\": \"工具擴展\"\n}\n"
  },
  {
    "path": "src/common/locales/data/zh-TW.ts",
    "content": "import { LocaleModel } from '@/common/locales/interface';\nimport messages from './zh-TW.json';\n\nconst model: LocaleModel = {\n  name: '繁體中文',\n  locale: 'zh-TW',\n  messages,\n  alias: ['tw'],\n};\n\nexport default model;\n"
  },
  {
    "path": "src/common/locales/index.test.ts",
    "content": "import { removeEmptyKeys } from './interface';\n\nit('test remove PR_IS_WELCOME', () => {\n  const messages = {\n    a: '1',\n    b: '',\n    c: '',\n  };\n\n  expect(removeEmptyKeys(messages, { b: '2' })).toEqual({\n    a: '1',\n    b: '2',\n  });\n});\n"
  },
  {
    "path": "src/common/locales/index.ts",
    "content": "import { LOCAL_USER_PREFERENCE_LOCALE_KEY } from '@/common/modelTypes/userPreference';\nimport { createIntlCache, createIntl, IntlShape, MessageDescriptor } from 'react-intl';\nimport { LocaleModel, removeEmptyKeys } from './interface';\nimport { localStorageService } from '@/common/chrome/storage';\n\nconst context = require.context('./data', true, /\\.[t|j]s$/);\n\nexport const locales = context.keys().map(key => {\n  const model = context(key).default as LocaleModel;\n  const en = context('./en-US.ts').default as LocaleModel;\n  return {\n    ...model,\n    messages: removeEmptyKeys(model.messages, en.messages),\n  };\n});\n\nexport const localesMap = locales.reduce((p, l) => {\n  p.set(l.locale, l);\n  return p;\n}, new Map<string, LocaleModel>());\n\nexport const getLanguage = () => {\n  const language = navigator.language;\n  for (const { locale, alias } of locales) {\n    if (locale === language || alias.some(o => o === language)) {\n      return locale;\n    }\n  }\n  return language;\n};\n\nclass LocaleService {\n  private intl?: IntlShape;\n  private _locale?: string;\n  async init() {\n    const locale = localStorageService.get(LOCAL_USER_PREFERENCE_LOCALE_KEY, getLanguage());\n    const messages = (localesMap.get(locale) || localesMap.get('en-US'))!.messages;\n    const cache = createIntlCache();\n    const intl = createIntl(\n      {\n        locale,\n        messages: messages,\n      },\n      cache\n    );\n    this.intl = intl;\n    this._locale = locale;\n  }\n\n  get locale() {\n    return this._locale ?? getLanguage();\n  }\n\n  format(descriptor: MessageDescriptor, values?: Record<string, any>): string {\n    if (!this.intl) {\n      throw Error('Should init intl before use');\n    }\n    return this.intl.formatMessage(descriptor, values);\n  }\n\n  getMessage(key: string): string {\n    return this.intl?.messages[key].toString() ?? '';\n  }\n}\n\nexport default new LocaleService();\n"
  },
  {
    "path": "src/common/locales/interface.ts",
    "content": "export interface LocaleModel {\n  name: string;\n  locale: string;\n  alias: string[];\n  messages: {\n    [key: string]: string;\n  };\n}\n\nexport function removeEmptyKeys(\n  params: LocaleModel['messages'],\n  defaultMessage: LocaleModel['messages']\n): LocaleModel['messages'] {\n  const result: LocaleModel['messages'] = {};\n  Object.keys(params).forEach(key => {\n    if (params[key] !== '') {\n      result[key] = params[key];\n    } else {\n      if (defaultMessage[key] !== '') {\n        result[key] = defaultMessage[key];\n      }\n    }\n  });\n  return result;\n}\n"
  },
  {
    "path": "src/common/matchUrl.test.ts",
    "content": "/* eslint-disable no-loop-func */\nimport matchUrl from './matchUrl';\n\ndescribe('test matchUrl', () => {\n  describe('should match all', () => {\n    const cases: { rule: string; true?: string[]; false?: string[] }[] = [\n      {\n        rule: '*://*/*',\n        true: ['http://www.google.com/', 'https://www.google.com/'],\n      },\n      {\n        rule: '*://docs.google.com/',\n        true: ['https://docs.google.com/'],\n        false: ['https://docs.google.com.cn/', 'https://sub.docs.google.com/'],\n      },\n      {\n        rule: '*://*.google.com/',\n        true: ['https://www.google.com/', 'https://a.b.google.com/', 'https://google.com/'],\n        false: ['https://www.google.com.hk/'],\n      },\n      {\n        rule: '*://www.google.tld/',\n        true: ['https://www.google.com/', 'https://www.google.com.cn/', 'https://www.google.jp/'],\n        false: ['https://www.google.example.com/'],\n      },\n      {\n        rule: 'https://www.google.com/a',\n        true: [\n          'https://www.google.com/a',\n          'https://www.google.com/a#hash',\n          'https://www.google.com/a?query',\n          'https://www.google.com/a?query#hash',\n        ],\n      },\n    ];\n    for (const iterator of cases) {\n      it(`test rune ${iterator.rule}`, () => {\n        if (Array.isArray(iterator.true)) {\n          for (const url of iterator.true) {\n            expect(matchUrl(iterator.rule, url)).toBeTruthy();\n          }\n        }\n        if (Array.isArray(iterator.false)) {\n          for (const url of iterator.false) {\n            expect(matchUrl(iterator.rule, url)).toBeFalsy();\n          }\n        }\n      });\n    }\n  });\n});\n"
  },
  {
    "path": "src/common/matchUrl.ts",
    "content": "import * as tld from 'tldjs';\n\nconst RE_MATCH_PARTS = /(.*?):\\/\\/([^/]*)\\/(.*)/;\n\nconst RE_HTTP_OR_HTTPS = /^https?$/i;\n\nfunction str2RE(str: string): string {\n  return str.replace(/([.?+[\\]{}()|^$])/g, '\\\\$1').replace(/\\*/g, '.*?');\n}\n\nfunction matchScheme(rule: string, data: string) {\n  if (rule === data) {\n    return true;\n  }\n  if (['*', 'http*'].includes(rule) && RE_HTTP_OR_HTTPS.test(data)) {\n    return 1;\n  }\n  return 0;\n}\n\nconst RE_STR_ANY = '(?:|.*?\\\\.)';\nconst RE_STR_TLD = '((?:\\\\.\\\\w+)+)';\nfunction hostMatcher(rule: string) {\n  let prefix = '';\n  let base = rule;\n  let suffix = '';\n  if (rule.startsWith('*.')) {\n    base = base.slice(2);\n    prefix = RE_STR_ANY;\n  }\n  if (rule.endsWith('.tld')) {\n    base = base.slice(0, -4);\n    suffix = RE_STR_TLD;\n  }\n  const re = new RegExp(`^${prefix}${str2RE(base)}${suffix}$`);\n  return (data: string) => {\n    if (rule === '*') {\n      return true;\n    }\n    if (rule === data) {\n      return true;\n    }\n    const matches = data.match(re);\n    if (matches) {\n      const [, tldStr] = matches;\n      if (!tldStr) {\n        return true;\n      }\n      const tldSuffix = tldStr.slice(1);\n      return tld.getPublicSuffix(tldSuffix) === tldSuffix;\n    }\n    return 0;\n  };\n}\n\nfunction pathMatcher(rule: string) {\n  const iHash = rule.indexOf('#');\n  let iQuery = rule.indexOf('?');\n  let strRe = str2RE(rule);\n  if (iQuery > iHash) iQuery = -1;\n  if (iHash < 0) {\n    if (iQuery < 0) strRe = `^${strRe}(?:[?#]|$)`;\n    else strRe = `^${strRe}(?:#|$)`;\n  }\n  const reRule = new RegExp(strRe);\n  return (data: string) => reRule.test(data);\n}\n\nfunction matchTester(rule: string) {\n  let test;\n  if (rule === '<all_urls>') {\n    test = () => true;\n  } else {\n    const ruleParts = rule.match(RE_MATCH_PARTS);\n    if (ruleParts) {\n      const matchHost = hostMatcher(ruleParts[2]);\n      const matchPath = pathMatcher(ruleParts[3]);\n      test = (url: string) => {\n        const parts = url.match(RE_MATCH_PARTS);\n        return (\n          !!ruleParts &&\n          !!parts &&\n          matchScheme(ruleParts[1], parts[1]) &&\n          matchHost(parts[2]) &&\n          matchPath(parts[3])\n        );\n      };\n    } else {\n      test = () => false;\n    }\n  }\n  return { test };\n}\n\nexport default (rule: string, url?: string) => {\n  if (!url) {\n    return false;\n  }\n  return matchTester(rule).test(url);\n};\n"
  },
  {
    "path": "src/common/modelTypes/account.ts",
    "content": "export interface AccountPreference {\n  [key: string]: string | undefined;\n  id: string;\n  type: string;\n  name: string;\n  avatar: string;\n  homePage: string;\n  description?: string;\n  defaultRepositoryId?: string;\n  imageHosting?: string;\n}\n\nexport interface AccountStore {\n  currentAccountId?: string;\n  defaultAccountId?: string;\n  accounts: AccountPreference[];\n}\n"
  },
  {
    "path": "src/common/modelTypes/clipper.ts",
    "content": "import { ClipperDataType } from '@/common/modelTypes/userPreference';\nimport {\n  Repository,\n  CompleteStatus,\n  CreateDocumentRequest,\n} from '@/common/backend/services/interface';\n\nexport interface ClipperHeaderForm {\n  [key: string]: string | number;\n  title: string;\n}\n\nexport interface ClipperStore {\n  clipperHeaderForm: ClipperHeaderForm;\n  url?: string;\n  currentAccountId: string;\n  repositories: Repository[];\n  currentImageHostingService?: { type: string };\n  currentRepository?: Repository;\n  clipperData: {\n    [key: string]: ClipperDataType;\n  };\n  completeStatus?: CompleteStatus;\n  createDocumentRequest?: CreateDocumentRequest;\n}\n"
  },
  {
    "path": "src/common/modelTypes/extensions.ts",
    "content": "import { IExtensionWithId } from '@/extensions/common';\n\nexport interface ExtensionStore {\n  extensions: IExtensionWithId[];\n  defaultExtensionId?: string | null;\n}\n\nexport const LOCAL_EXTENSIONS_DISABLED_EXTENSIONS_KEY = 'local.extensions.disabled.extensions';\n\nexport const LOCAL_EXTENSIONS_ENABLE_AUTOMATIC_EXTENSIONS_KEY =\n  'local.extensions.enable.automatic.extensions';\n"
  },
  {
    "path": "src/common/modelTypes/userPreference.ts",
    "content": "import { ServiceMeta, ImageHostingServiceMeta } from '@/common/backend';\n\nexport interface UserPreferenceStore {\n  locale: string;\n  imageHosting: ImageHosting[];\n  liveRendering: boolean;\n  iconColor: 'dark' | 'light' | 'auto';\n  servicesMeta: {\n    [type: string]: ServiceMeta;\n  };\n  imageHostingServicesMeta: {\n    [type: string]: ImageHostingServiceMeta;\n  };\n}\n\n/**\n * 图床配置的数据结构\n */\nexport interface ImageHosting {\n  id: string;\n  type: string;\n  remark?: string;\n  info?: {\n    [key: string]: string;\n  };\n}\n\nexport interface ImageClipperData {\n  dataUrl: string;\n  width: number;\n  height: number;\n}\n\nexport type ClipperDataType = string | ImageClipperData;\n\nexport const LOCAL_USER_PREFERENCE_LOCALE_KEY = 'local.userPreference.locale';\n\n/**\n * user Access Tiken\n */\nexport const LOCAL_ACCESS_TOKEN_LOCALE_KEY = 'local.access.token.locale';\n"
  },
  {
    "path": "src/common/object.ts",
    "content": "export function isUndefined(data: any) {\n  // eslint-disable-next-line no-undefined\n  return data === undefined;\n}\n"
  },
  {
    "path": "src/common/storage/__test__/index.spec.ts",
    "content": "import { PreferenceStorage, TypedCommonStorageInterface, CommonStorage } from './../interface';\n/* eslint-disable max-nested-callbacks */\n/* eslint-disable no-undefined */\nimport update from 'immutability-helper';\nimport { TypedCommonStorage } from '../typedCommonStorage';\n\nclass MockStorage implements CommonStorage {\n  private data: any;\n  constructor() {\n    this.data = {};\n  }\n\n  get = (key: string) => {\n    return this.data[key];\n  };\n\n  set = (key: string, item: Object) => {\n    this.data[key] = item;\n  };\n}\n\ndescribe('test storage', () => {\n  let storage: TypedCommonStorageInterface;\n  const defaultPreference: PreferenceStorage = {\n    imageHosting: [],\n    defaultPluginId: undefined,\n    showLineNumber: true,\n    liveRendering: true,\n    iconColor: 'auto',\n  };\n\n  beforeEach(() => {\n    storage = new TypedCommonStorage(new MockStorage());\n  });\n\n  it('The default value should be correct', async () => {\n    expect(await storage.getPreference()).toEqual(defaultPreference);\n    expect(await storage.getDefaultPluginId()).toEqual(defaultPreference.defaultPluginId);\n    expect(await storage.getShowLineNumber()).toEqual(defaultPreference.showLineNumber);\n    expect(await storage.getLiveRendering()).toEqual(defaultPreference.liveRendering);\n  });\n\n  it('setDefaultPluginId should work correctly', async () => {\n    for (const value of ['11', '22', 'data', null]) {\n      await storage.setDefaultPluginId(value);\n      expect(await storage.getDefaultPluginId()).toBe(value);\n      expect(await storage.getPreference()).toEqual(\n        update(defaultPreference, {\n          defaultPluginId: {\n            $set: value,\n          },\n        })\n      );\n    }\n  });\n\n  it('setShowLineNumber should work correctly', async () => {\n    for (const value of [true, false, true]) {\n      await storage.setShowLineNumber(value);\n      expect(await storage.getShowLineNumber()).toBe(value);\n      expect(await storage.getPreference()).toEqual(\n        update(defaultPreference, {\n          showLineNumber: {\n            $set: value,\n          },\n        })\n      );\n    }\n  });\n\n  it('setLiveRendering should work correctly', async () => {\n    for (const value of [true, false, true]) {\n      await storage.setLiveRendering(value);\n      expect(await storage.getLiveRendering()).toBe(value);\n      expect(await storage.getPreference()).toEqual(\n        update(defaultPreference, {\n          liveRendering: {\n            $set: value,\n          },\n        })\n      );\n    }\n  });\n});\n"
  },
  {
    "path": "src/common/storage/index.ts",
    "content": "import { TypedCommonStorageInterface, CommonStorage } from './interface';\nexport * from './interface';\nimport * as browser from '@web-clipper/chrome-promise';\nimport { TypedCommonStorage } from './typedCommonStorage';\n\nexport class ChromeSyncStorageImpl implements CommonStorage {\n  public async set(key: string, item: Object): Promise<void> {\n    let tempObject: any = {};\n    tempObject[key] = item;\n    return browser.storage.sync.set(tempObject);\n  }\n\n  public async get<T>(key: string): Promise<T> {\n    const items = await browser.storage.sync.get(key);\n    return items[key];\n  }\n}\n\nexport default new TypedCommonStorage(new ChromeSyncStorageImpl()) as TypedCommonStorageInterface;\n"
  },
  {
    "path": "src/common/storage/interface.ts",
    "content": "import { ImageHosting } from 'common/types';\nexport interface PreferenceStorage {\n  imageHosting: ImageHosting[];\n  defaultPluginId?: string | null;\n  showLineNumber: boolean;\n  liveRendering: boolean;\n  iconColor: 'dark' | 'light' | 'auto';\n}\n\nexport interface CommonStorage {\n  set(key: string, value: any): void | Promise<void>;\n  get<T>(key: string): Promise<T | undefined>;\n}\n\nexport interface TypedCommonStorageInterface {\n  getPreference(): Promise<PreferenceStorage>;\n\n  /** --------默认插件--------- */\n\n  setDefaultPluginId(id: string | null): Promise<void>;\n\n  getDefaultPluginId(): Promise<string | undefined | null>;\n\n  /** --------编辑器显示行号--------- */\n\n  setShowLineNumber(value: boolean): Promise<void>;\n\n  getShowLineNumber(): Promise<boolean>;\n\n  /** --------实时渲染--------- */\n  setLiveRendering(value: boolean): Promise<void>;\n\n  getLiveRendering(): Promise<boolean>;\n\n  setIconColor(value: string): Promise<void>;\n\n  getIconColor(): Promise<string>;\n\n  /** --------图床--------- */\n\n  addImageHosting(imageHosting: ImageHosting): Promise<ImageHosting[]>;\n\n  getImageHosting(): Promise<ImageHosting[]>;\n\n  deleteImageHostingById(id: string): Promise<ImageHosting[]>;\n\n  editImageHostingById(id: string, value: ImageHosting): Promise<ImageHosting[]>;\n}\n"
  },
  {
    "path": "src/common/storage/typedCommonStorage.ts",
    "content": "import { ImageHosting } from '@/common/types';\nimport { TypedCommonStorageInterface, CommonStorage, PreferenceStorage } from './interface';\n\nconst keysOfStorage = {\n  accounts: 'accounts',\n  defaultAccountId: 'defaultAccountId',\n  defaultPluginId: 'defaultPluginId',\n  showQuickResponseCode: 'showQuickResponseCode',\n  liveRendering: 'liveRendering',\n  showLineNumber: 'showLineNumber',\n  imageHosting: 'imageHosting',\n  iconColor: 'iconColor',\n};\n\nexport class TypedCommonStorage implements TypedCommonStorageInterface {\n  store: CommonStorage;\n\n  constructor(store: CommonStorage) {\n    this.store = store;\n  }\n\n  getPreference = async (): Promise<PreferenceStorage> => {\n    const defaultPluginId = await this.getDefaultPluginId();\n    const showLineNumber = await this.getShowLineNumber();\n    const liveRendering = await this.getLiveRendering();\n    const imageHosting = await this.getImageHosting();\n    const iconColor = await this.getIconColor();\n    return {\n      defaultPluginId,\n      showLineNumber,\n      liveRendering,\n      imageHosting,\n      iconColor,\n    };\n  };\n\n  setDefaultPluginId = async (value: string | null) => {\n    await this.store.set(keysOfStorage.defaultPluginId, value);\n  };\n  getDefaultPluginId = async () => {\n    return this.store.get<string>(keysOfStorage.defaultPluginId);\n  };\n\n  setShowLineNumber = async (value: boolean) => {\n    await this.store.set(keysOfStorage.showLineNumber, value);\n  };\n  getShowLineNumber = async () => {\n    const value = await this.store.get<boolean>(keysOfStorage.showLineNumber);\n    return value !== false;\n  };\n\n  setLiveRendering = async (value: boolean) => {\n    await this.store.set(keysOfStorage.liveRendering, value);\n  };\n  getLiveRendering = async () => {\n    const value = await this.store.get<boolean>(keysOfStorage.liveRendering);\n    return value !== false;\n  };\n\n  setIconColor = async (value: string) => {\n    await this.store.set(keysOfStorage.iconColor, value);\n  };\n  getIconColor = async () => {\n    const value = await this.store.get<'dark' | 'light' | 'auto'>(keysOfStorage.iconColor);\n    return value ?? 'auto';\n  };\n\n  addImageHosting = async (imageHosting: ImageHosting) => {\n    const imageHostingList = await this.getImageHosting();\n    if (imageHostingList.some(o => o.id === imageHosting.id)) {\n      throw new Error('Do not allow duplicate image hosting');\n    }\n    imageHostingList.push(imageHosting);\n    await this.store.set('imageHosting', imageHostingList);\n    return imageHostingList;\n  };\n\n  getImageHosting = async () => {\n    const value = await this.store.get<ImageHosting[]>('imageHosting');\n    if (!value) {\n      return [];\n    }\n    return value;\n  };\n\n  deleteImageHostingById = async (id: string) => {\n    const imageHostingList = await this.getImageHosting();\n    const newImageHostingList = imageHostingList.filter(imageHosting => imageHosting.id !== id);\n    await this.store.set(keysOfStorage.imageHosting, newImageHostingList);\n    return newImageHostingList;\n  };\n\n  editImageHostingById = async (id: string, value: ImageHosting) => {\n    const imageHostingList = await this.getImageHosting();\n    const index = imageHostingList.findIndex(imageHosting => imageHosting.id === id);\n    if (index < 0) {\n      throw new Error('图床不存在');\n    }\n    imageHostingList[index] = value;\n    await this.store.set('imageHosting', imageHostingList);\n    return imageHostingList;\n  };\n}\n"
  },
  {
    "path": "src/common/strings.ts",
    "content": "/*---------------------------------------------------------------------------------------------\n *  Copyright (c) Microsoft Corporation. All rights reserved.\n *  Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nexport const enum Constants {\n  /**\n   * MAX SMI (SMall Integer) as defined in v8.\n   * one bit is lost for boxing/unboxing flag.\n   * one bit is lost for sign flag.\n   * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values\n   */\n  MAX_SAFE_SMALL_INTEGER = 1 << 30,\n\n  /**\n   * MIN SMI (SMall Integer) as defined in v8.\n   * one bit is lost for boxing/unboxing flag.\n   * one bit is lost for sign flag.\n   * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values\n   */\n  MIN_SAFE_SMALL_INTEGER = -(1 << 30),\n\n  /**\n   * Max unsigned integer that fits on 8 bits.\n   */\n  MAX_UINT_8 = 255, // 2^8 - 1\n\n  /**\n   * Max unsigned integer that fits on 16 bits.\n   */\n  MAX_UINT_16 = 65535, // 2^16 - 1\n\n  /**\n   * Max unsigned integer that fits on 32 bits.\n   */\n  MAX_UINT_32 = 4294967295, // 2^32 - 1\n\n  UNICODE_SUPPLEMENTARY_PLANE_BEGIN = 0x010000,\n}\n/**\n * A manual encoding of `str` to UTF8.\n * Use only in environments which do not offer native conversion methods!\n */\nexport function encodeUTF8(str: string): Uint8Array {\n  const strLen = str.length;\n\n  // See https://en.wikipedia.org/wiki/UTF-8\n\n  // first loop to establish needed buffer size\n  let neededSize = 0;\n  let strOffset = 0;\n  while (strOffset < strLen) {\n    const codePoint = getNextCodePoint(str, strLen, strOffset);\n    strOffset += codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1;\n\n    if (codePoint < 0x0080) {\n      neededSize += 1;\n    } else if (codePoint < 0x0800) {\n      neededSize += 2;\n    } else if (codePoint < 0x10000) {\n      neededSize += 3;\n    } else {\n      neededSize += 4;\n    }\n  }\n\n  // second loop to actually encode\n  const arr = new Uint8Array(neededSize);\n  strOffset = 0;\n  let arrOffset = 0;\n  while (strOffset < strLen) {\n    const codePoint = getNextCodePoint(str, strLen, strOffset);\n    strOffset += codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1;\n\n    if (codePoint < 0x0080) {\n      arr[arrOffset++] = codePoint;\n    } else if (codePoint < 0x0800) {\n      arr[arrOffset++] = 0b11000000 | ((codePoint & 0b00000000000000000000011111000000) >>> 6);\n      arr[arrOffset++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0);\n    } else if (codePoint < 0x10000) {\n      arr[arrOffset++] = 0b11100000 | ((codePoint & 0b00000000000000001111000000000000) >>> 12);\n      arr[arrOffset++] = 0b10000000 | ((codePoint & 0b00000000000000000000111111000000) >>> 6);\n      arr[arrOffset++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0);\n    } else {\n      arr[arrOffset++] = 0b11110000 | ((codePoint & 0b00000000000111000000000000000000) >>> 18);\n      arr[arrOffset++] = 0b10000000 | ((codePoint & 0b00000000000000111111000000000000) >>> 12);\n      arr[arrOffset++] = 0b10000000 | ((codePoint & 0b00000000000000000000111111000000) >>> 6);\n      arr[arrOffset++] = 0b10000000 | ((codePoint & 0b00000000000000000000000000111111) >>> 0);\n    }\n  }\n\n  return arr;\n}\n/**\n * A manual decoding of a UTF8 string.\n * Use only in environments which do not offer native conversion methods!\n */\nexport function decodeUTF8(buffer: Uint8Array): string {\n  // https://en.wikipedia.org/wiki/UTF-8\n\n  const len = buffer.byteLength;\n  const result: string[] = [];\n  let offset = 0;\n  while (offset < len) {\n    const v0 = buffer[offset];\n    let codePoint: number;\n    if (v0 >= 0b11110000 && offset + 3 < len) {\n      // 4 bytes\n      codePoint =\n        (((buffer[offset++] & 0b00000111) << 18) >>> 0) |\n        (((buffer[offset++] & 0b00111111) << 12) >>> 0) |\n        (((buffer[offset++] & 0b00111111) << 6) >>> 0) |\n        (((buffer[offset++] & 0b00111111) << 0) >>> 0);\n    } else if (v0 >= 0b11100000 && offset + 2 < len) {\n      // 3 bytes\n      codePoint =\n        (((buffer[offset++] & 0b00001111) << 12) >>> 0) |\n        (((buffer[offset++] & 0b00111111) << 6) >>> 0) |\n        (((buffer[offset++] & 0b00111111) << 0) >>> 0);\n    } else if (v0 >= 0b11000000 && offset + 1 < len) {\n      // 2 bytes\n      codePoint =\n        (((buffer[offset++] & 0b00011111) << 6) >>> 0) |\n        (((buffer[offset++] & 0b00111111) << 0) >>> 0);\n    } else {\n      // 1 byte\n      codePoint = buffer[offset++];\n    }\n\n    if ((codePoint >= 0 && codePoint <= 0xd7ff) || (codePoint >= 0xe000 && codePoint <= 0xffff)) {\n      // Basic Multilingual Plane\n      result.push(String.fromCharCode(codePoint));\n    } else if (codePoint >= 0x010000 && codePoint <= 0x10ffff) {\n      // Supplementary Planes\n      const uPrime = codePoint - 0x10000;\n      const w1 = 0xd800 + ((uPrime & 0b11111111110000000000) >>> 10);\n      const w2 = 0xdc00 + ((uPrime & 0b00000000001111111111) >>> 0);\n      result.push(String.fromCharCode(w1));\n      result.push(String.fromCharCode(w2));\n    } else {\n      // illegal code point\n      result.push(String.fromCharCode(0xfffd));\n    }\n  }\n\n  return result.join('');\n}\n\nexport function getNextCodePoint(str: string, len: number, offset: number): number {\n  const charCode = str.charCodeAt(offset);\n  if (isHighSurrogate(charCode) && offset + 1 < len) {\n    const nextCharCode = str.charCodeAt(offset + 1);\n    if (isLowSurrogate(nextCharCode)) {\n      return computeCodePoint(charCode, nextCharCode);\n    }\n  }\n  return charCode;\n}\n\n/**\n * See http://en.wikipedia.org/wiki/Surrogate_pair\n */\nexport function isHighSurrogate(charCode: number): boolean {\n  return 0xd800 <= charCode && charCode <= 0xdbff;\n}\n\n/**\n * See http://en.wikipedia.org/wiki/Surrogate_pair\n */\nexport function isLowSurrogate(charCode: number): boolean {\n  return 0xdc00 <= charCode && charCode <= 0xdfff;\n}\n\n/**\n * See http://en.wikipedia.org/wiki/Surrogate_pair\n */\nexport function computeCodePoint(highSurrogate: number, lowSurrogate: number): number {\n  return ((highSurrogate - 0xd800) << 10) + (lowSurrogate - 0xdc00) + 0x10000;\n}\n"
  },
  {
    "path": "src/common/types.ts",
    "content": "import { AccountStore } from './modelTypes/account';\nimport { RouteComponentProps } from 'react-router';\nimport { Dispatch } from 'react';\nimport { UserPreferenceStore } from '@/common/modelTypes/userPreference';\nimport { ClipperStore } from '@/common/modelTypes/clipper';\nimport { DvaLoadingState } from 'dva-loading';\nimport { ExtensionStore } from './modelTypes/extensions';\n\nexport * from '@/common/modelTypes/userPreference';\nexport * from '@/common/modelTypes/clipper';\nexport * from '@/common/modelTypes/account';\n\nexport type DvaRouterProps = {\n  dispatch: Dispatch<any>;\n} & RouteComponentProps;\n\ninterface DvaLoadingState {\n  global: boolean;\n  models: { [type: string]: boolean | undefined };\n  effects: { [type: string]: boolean | undefined };\n}\n\nexport interface GlobalStore {\n  account: AccountStore;\n  clipper: ClipperStore;\n  userPreference: UserPreferenceStore;\n  loading: DvaLoadingState;\n  extension: ExtensionStore;\n  router: {\n    location: {\n      search: string;\n      pathname: string;\n    };\n  };\n}\n\nexport interface IResponse<T> {\n  result: T;\n  message: string;\n}\n"
  },
  {
    "path": "src/common/version/index.test.ts",
    "content": "import { hasUpdate } from './index';\n\ndescribe('test version', function() {\n  it('test hasUpdate', function() {\n    expect(hasUpdate('3.0.1', '3.0.0')).toBe(true);\n    expect(hasUpdate('3.1.1', '3.0.1')).toBe(true);\n    expect(hasUpdate('3.0.0', '2.0.0')).toBe(true);\n    expect(hasUpdate('3.0.0', '3.0.0')).toBe(false);\n    expect(hasUpdate('3.0.0', '4.0.0')).toBe(false);\n  });\n});\n"
  },
  {
    "path": "src/common/version/index.ts",
    "content": "export function hasUpdate(remote: string, local: string): boolean {\n  if (!remote) {\n    return false;\n  }\n  const remoteVersion = remote.split('.').map(version => parseInt(version, 10));\n  const localVersion = local.split('.').map(version => parseInt(version, 10));\n  for (let i = 0; i < remoteVersion.length; i++) {\n    if (remoteVersion[i] > localVersion[i]) {\n      return true;\n    }\n    if (remoteVersion[i] < localVersion[i]) {\n      return false;\n    }\n  }\n  return false;\n}\n"
  },
  {
    "path": "src/components/ExtensionCard/index.less",
    "content": ":global {\n  .ant-form-item {\n    margin-bottom: 8px;\n  }\n}\n"
  },
  {
    "path": "src/components/ExtensionCard/index.tsx",
    "content": "import React from 'react';\nimport { Button, Card, Modal, Select } from 'antd';\nimport { FormattedMessage } from 'react-intl';\nimport { SerializedExtensionInfo } from '@/extensions/common';\nimport IconFont from '@/components/IconFont';\nimport { SettingOutlined } from '@ant-design/icons';\nimport { Form, FormItem, Input as FormInput } from '@formily/antd';\nimport { createForm, onFormValuesChange } from '@formily/core';\nimport { createSchemaField } from '@formily/react';\nimport { toJS } from 'mobx';\nimport Container from 'typedi';\nimport { IExtensionContainer, IExtensionService } from '@/service/common/extension';\nimport useFilterExtensions from '@/common/hooks/useFilterExtensions';\nimport './index.less';\nimport localeService from '@/common/locales/index';\n\ninterface ExtensionCardProps {\n  manifest: SerializedExtensionInfo['manifest'];\n  actions?: React.ReactNode[];\n  className?: string;\n}\n\nconst ExtensionSelect: React.FC<{ value: string; onChange: any }> = ({ value, onChange }) => {\n  const extensionContainer = Container.get(IExtensionContainer);\n  const [, clipExtensions] = useFilterExtensions(extensionContainer.extensions);\n\n  return (\n    <Select\n      mode=\"multiple\"\n      value={value}\n      onChange={onChange}\n      options={clipExtensions.map(o => ({\n        title: o.manifest.name,\n        value: o.id,\n        key: o.id,\n      }))}\n    ></Select>\n  );\n};\n\nconst SchemaField = createSchemaField({\n  components: {\n    FormItem,\n    textarea: FormInput.TextArea!,\n    clipExtensionsSelect: ExtensionSelect,\n  },\n});\n\nconst ReachableContext = React.createContext<{\n  manifest: SerializedExtensionInfo['manifest'] | null;\n  // eslint-disable-next-line no-undefined\n}>({ manifest: null });\n\nconst config = () => {\n  return {\n    width: 800,\n    content: (\n      <>\n        <ReachableContext.Consumer>\n          {({ manifest }) => {\n            const config = manifest!.config;\n            const extensionId: string = manifest!.extensionId as string;\n            const defaultValue =\n              Container.get(IExtensionService).getExtensionConfig(extensionId) ||\n              toJS(config?.default);\n            const normalForm = createForm({\n              validateFirst: true,\n              initialValues: defaultValue as any,\n              effects: () => {\n                onFormValuesChange(form => {\n                  if (form.mounted) {\n                    Container.get(IExtensionService).setExtensionConfig(extensionId, form.values);\n                  }\n                });\n              },\n            });\n            return (\n              <div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>\n                <div style={{ width: '600px' }}>\n                  <Form form={normalForm} layout=\"vertical\">\n                    <SchemaField schema={config!.scheme} />\n                  </Form>\n                  <Button\n                    onClick={() => {\n                      normalForm.setValues(toJS(config?.default), 'overwrite');\n                    }}\n                  >\n                    {localeService.format({ id: 'preference.extensions.form.reset' })}\n                  </Button>\n                </div>\n              </div>\n            );\n          }}\n        </ReachableContext.Consumer>\n      </>\n    ),\n  };\n};\n\nconst ExtensionCard: React.FC<ExtensionCardProps> = ({ manifest, actions, className }) => {\n  const extra: React.ReactNode[] = [manifest.version];\n  const [modal, contextHolder] = Modal.useModal();\n\n  if (manifest.config) {\n    extra.push(\n      <SettingOutlined\n        style={{ marginLeft: 8 }}\n        key=\"setting\"\n        onClick={() => {\n          modal.info(config());\n        }}\n      />\n    );\n  }\n  return (\n    <React.Fragment>\n      <ReachableContext.Provider value={{ manifest: manifest }}>\n        {contextHolder}\n      </ReachableContext.Provider>\n      <Card\n        className={className}\n        actions={actions}\n        extra={[extra]}\n        title={<Card.Meta avatar={<IconFont type={manifest.icon} />} title={manifest.name} />}\n      >\n        <div style={{ height: 30 }}>\n          {manifest.description || <FormattedMessage id=\"preference.extensions.no.Description\" />}\n        </div>\n      </Card>\n    </React.Fragment>\n  );\n};\n\nexport default ExtensionCard;\n"
  },
  {
    "path": "src/components/IconFont.tsx",
    "content": "import React from 'react';\nimport { Icon as LegacyIcon } from '@ant-design/compatible';\nimport { IconProps } from '@ant-design/compatible/es/icon';\nimport { createFromIconfontCN } from '@ant-design/icons';\nimport Container from 'typedi';\nimport { IConfigService } from '@/service/common/config';\nimport { Observer, useObserver } from 'mobx-react';\n\nconst IconFont: React.FC<IconProps> = (props) => {\n  const configService = Container.get(IConfigService);\n  const IconFont = useObserver(() => {\n    return createFromIconfontCN({ scriptUrl: './icon.js' });\n  });\n  return (\n    <Observer>\n      {() => {\n        if (!configService.remoteIconSet.has(props.type)) {\n          return <LegacyIcon {...props} />;\n        }\n        if (!props.type) {\n          throw new Error('Type is required');\n        }\n        return <IconFont {...props} type={props.type!} />;\n      }}\n    </Observer>\n  );\n};\n\nexport default IconFont;\n"
  },
  {
    "path": "src/components/ImageHostingSelect.less",
    "content": ".imageHostingSelect {\n  :global {\n    .ant-select-selector {\n      height: 72px !important;\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/ImageHostingSelect.tsx",
    "content": "import { ImageHostingWithMeta } from '@/common/hooks/useFilterImageHostingServices';\nimport Select, { SelectProps } from 'antd/lib/select';\nimport React, { forwardRef } from 'react';\nimport ImageHostingSelectOption from '@/components/imageHostingSelectOption';\nimport styles from './ImageHostingSelect.less';\n\ninterface ImageHostingSelectProps extends SelectProps<string> {\n  supportedImageHostingServices: ImageHostingWithMeta[];\n}\n\n/**\n * TODO\n * fix any\n */\nexport const ImageHostingSelect: React.ForwardRefRenderFunction<any, ImageHostingSelectProps> = (\n  { supportedImageHostingServices, ...props },\n  ref\n) => (\n  <Select allowClear className={styles.imageHostingSelect} {...props} ref={ref}>\n    {supportedImageHostingServices.map(({ imageHostingServices: { id, remark }, meta }) => {\n      return (\n        <Select.Option key={id} value={id}>\n          <ImageHostingSelectOption id={id} icon={meta.icon} name={meta.name} remark={remark} />\n        </Select.Option>\n      );\n    })}\n  </Select>\n);\n\n/**\n * TODO\n * fix any\n */\nexport default forwardRef<any, ImageHostingSelectProps>(ImageHostingSelect);\n"
  },
  {
    "path": "src/components/LinkRender/index.tsx",
    "content": "import React from 'react';\n\ninterface LinkRenderProps {\n  href: string;\n}\n\nconst LinkRender: React.FC<LinkRenderProps> = props => {\n  return (\n    <a href={props.href} target=\"_blank\">\n      {props.children}\n    </a>\n  );\n};\n\nexport default LinkRender;\n"
  },
  {
    "path": "src/components/RepositorySelect.tsx",
    "content": "import React, { useMemo, useState, forwardRef, useCallback } from 'react';\nimport { Select } from 'antd';\nimport { Repository } from 'common/backend';\nimport { SelectProps } from 'antd/lib/select';\nimport { debounce } from 'lodash';\n\ninterface RepositoryInGroup {\n  [groupId: string]: {\n    groupId: string;\n    groupName: string;\n    repositories: Repository[];\n  };\n}\n\ninterface RepositorySelectProps extends SelectProps<string> {\n  repositories: Repository[];\n}\n\nconst RepositorySelect: React.FC<RepositorySelectProps> = ({ repositories, ...props }, ref) => {\n  const [searchKey, _setSearchKey] = useState<string>();\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const setSearchKey = useCallback(debounce(_setSearchKey, repositories.length > 100 ? 500 : 0), [\n    _setSearchKey,\n    repositories,\n  ]);\n\n  const repositoryInGroup = useMemo(() => {\n    const repositoryInGroup: RepositoryInGroup = {};\n    repositories.forEach(o => {\n      if (searchKey) {\n        if (!o.name.toLocaleLowerCase().includes(searchKey.toLocaleLowerCase())) {\n          return;\n        }\n      }\n      let group = repositoryInGroup[o.groupId];\n      if (group) {\n        group.repositories.push(o);\n      } else {\n        repositoryInGroup[o.groupId] = {\n          groupId: o.groupId,\n          groupName: o.groupName,\n          repositories: [o],\n        };\n      }\n    });\n    return repositoryInGroup;\n  }, [repositories, searchKey]);\n\n  return (\n    <Select {...props} allowClear showSearch onSearch={setSearchKey} ref={ref} filterOption={false}>\n      {Object.values(repositoryInGroup).map(group => (\n        <Select.OptGroup key={group.groupId} label={group.groupName}>\n          {group.repositories.map(({ id, name, disabled }) => (\n            <Select.Option disabled={disabled} key={id} value={id}>\n              {name}\n            </Select.Option>\n          ))}\n        </Select.OptGroup>\n      ))}\n    </Select>\n  );\n};\n\nexport default (forwardRef(RepositorySelect as any) as unknown) as typeof RepositorySelect;\n"
  },
  {
    "path": "src/components/accountItem/index.less",
    "content": ".card {\n  padding: 10px;\n  border-radius: 10px;\n  text-align: center;\n  line-height: 1.5;\n  font-size: 14px;\n  border: 1px solid #e8e8e8;\n  height: 300px;\n  position: relative;\n  display: flex;\n  flex-direction: column;\n}\n\n.star {\n  position: absolute;\n  top: 10px;\n  right: 10px;\n  font-size: 20px;\n}\n\n.userInfo {\n  flex: 1;\n  .name {\n    margin-bottom: 4px;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n    font-size: 20px;\n    color: #262626;\n  }\n  .description {\n    color: #8c8c8c;\n    margin-bottom: 8px;\n    height: 63px;\n    overflow: hidden;\n    word-wrap: break-word;\n  }\n}\n\n.operation {\n  display: flex;\n  .editButton {\n    flex: 1;\n    margin-right: 10px;\n  }\n}\n"
  },
  {
    "path": "src/components/accountItem/index.tsx",
    "content": "import * as React from 'react';\nimport { StarOutlined } from '@ant-design/icons';\nimport { Button } from 'antd';\nimport styles from './index.less';\nimport { FormattedMessage } from 'react-intl';\nimport IconAvatar from '@/components/avatar';\n\ninterface PageProps {\n  isDefault: boolean;\n  id: string;\n  name: string;\n  avatar: string;\n  description?: string;\n  onEdit(id: string): void;\n  onDelete(id: string): void;\n  onSetDefaultAccount(id: string): void;\n}\n\nexport default class Page extends React.Component<PageProps> {\n  handleEdit = () => {\n    this.props.onEdit(this.props.id);\n  };\n\n  handleDelete = () => {\n    this.props.onDelete(this.props.id);\n  };\n\n  handleSetDefaultAccount = () => {\n    this.props.onSetDefaultAccount(this.props.id);\n  };\n\n  render() {\n    const { name, description, avatar, isDefault } = this.props;\n    let tagStyle;\n    if (isDefault) {\n      tagStyle = { color: 'red' };\n    }\n    return (\n      <div className={styles.card}>\n        <div className={styles.star}>\n          <StarOutlined style={tagStyle} onClick={this.handleSetDefaultAccount} />\n        </div>\n        <div className={styles.userInfo}>\n          <IconAvatar size={96} avatar={avatar} icon={avatar} />\n          <div className={styles.name}>{name}</div>\n          <div className={styles.description}>{description}</div>\n        </div>\n        <div className={styles.operation}>\n          <Button className={styles.editButton} type=\"primary\" onClick={this.handleEdit}>\n            <FormattedMessage id=\"component.accountItem.edit\"></FormattedMessage>\n          </Button>\n          <Button type=\"primary\" danger onClick={this.handleDelete}>\n            <FormattedMessage id=\"component.accountItem.delete\" />\n          </Button>\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "src/components/avatar/index.tsx",
    "content": "import React from 'react';\nimport { Avatar } from 'antd';\nimport IconFont from '@/components/IconFont';\n\ninterface IconAvatarProps {\n  avatar?: string;\n  icon: string;\n  size: 'large' | 'small' | number;\n}\n\nconst IconAvatar: React.FC<IconAvatarProps> = ({ avatar, size, icon: _icon }) => {\n  const icon = avatar || _icon;\n  let fontSize;\n  if (typeof size === 'string') {\n    fontSize = {\n      small: 24,\n      large: 40,\n    }[size];\n  } else {\n    fontSize = size;\n  }\n  if (icon.startsWith('http') || icon.indexOf('/') != -1) {\n    return <Avatar size={fontSize} src={icon} />;\n  }\n  return <IconFont style={{ fontSize }} type={icon} />;\n};\n\nexport default IconAvatar;\n"
  },
  {
    "path": "src/components/container/index.less",
    "content": "@import '~antd/es//style/themes/default.less';\n\n.mainContainer {\n  position: fixed;\n  right: 10px;\n  top: 10px;\n  box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px;\n}\n\n.toolContainer {\n  width: 324px;\n  height: auto;\n  background: #fff;\n  padding: 10px;\n}\n.closeButton {\n  position: absolute;\n  right: 10px;\n  top: 10px;\n  cursor: pointer;\n}\n\n.centerContainer {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  width: 100%;\n  height: 100%;\n}\n.editorContainer {\n  position: absolute;\n  right: 350px;\n  top: 0;\n  box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px;\n  border: 2px solid #dddddd;\n}\n\n.mask {\n  position: fixed;\n  right: 0px;\n  top: 0px;\n  bottom: 0px;\n  left: 0px;\n  background: @modal-mask-bg;\n}\n"
  },
  {
    "path": "src/components/container/index.tsx",
    "content": "import React from 'react';\nimport styles from './index.less';\nimport { CloseOutlined } from '@ant-design/icons';\n\nconst Container: React.FC = ({ children }) => {\n  return <div className={styles.mainContainer}>{children}</div>;\n};\n\nexport interface ToolContainerProps {\n  onClickCloseButton?: () => void;\n  onClickMask?: () => void;\n}\n\nexport class ToolContainer extends React.Component<ToolContainerProps> {\n  onClickCloseButton = () => {\n    if (this.props.onClickCloseButton) {\n      this.props.onClickCloseButton();\n    }\n  };\n\n  handleClickMask = () => {\n    if (this.props.onClickMask) {\n      this.props.onClickMask();\n    }\n  };\n\n  public render() {\n    return (\n      <React.Fragment>\n        <div className={styles.mask} onClick={this.handleClickMask}></div>\n        <Container>\n          <div className={styles.toolContainer}>\n            <div className={styles.closeButton} onClick={this.onClickCloseButton}>\n              <CloseOutlined />\n            </div>\n            {<div>{this.props.children}</div>}\n          </div>\n        </Container>\n      </React.Fragment>\n    );\n  }\n}\n\nexport const CenterContainer: React.FC = ({ children }) => {\n  return <div className={styles.centerContainer}>{children}</div>;\n};\n\nexport const EditorContainer: React.FC = ({ children }) => {\n  return <div className={styles.editorContainer}>{children}</div>;\n};\n"
  },
  {
    "path": "src/components/imageHostingSelectOption/index.less",
    "content": ".avatar {\n  img {\n    max-width: 32px;\n    max-height: 32px;\n    height: auto;\n  }\n}\n"
  },
  {
    "path": "src/components/imageHostingSelectOption/index.tsx",
    "content": "import * as React from 'react';\nimport { List, Avatar } from 'antd';\nimport styles from './index.less';\nimport { FormattedMessage } from 'react-intl';\nimport IconFont from '@/components/IconFont';\n\ninterface PageProps {\n  icon: string;\n  name: string;\n  remark?: string;\n  id: string;\n}\n\nexport default class Page extends React.Component<PageProps> {\n  render() {\n    const {\n      name,\n      remark = (\n        <FormattedMessage\n          id=\"component.imageHostingSelectOption.noDescription\"\n          defaultMessage=\"No Description\"\n        />\n      ),\n      icon,\n    } = this.props;\n    let avatar;\n\n    if (icon.startsWith('http') || icon.indexOf('/') != -1) {\n      avatar = <Avatar src={icon} className={styles.avatar} />;\n    } else {\n      avatar = <IconFont type={icon} style={{ fontSize: 32 }} />;\n    }\n\n    return (\n      <List.Item>\n        <List.Item.Meta avatar={avatar} title={<div>{name}</div>} description={remark} />\n      </List.Item>\n    );\n  }\n}\n"
  },
  {
    "path": "src/components/imagehostingListItem/index.less",
    "content": ".avatar {\n  img {\n    max-width: 32px;\n    max-height: 32px;\n    height: auto;\n  }\n}\n"
  },
  {
    "path": "src/components/imagehostingListItem/index.tsx",
    "content": "import * as React from 'react';\nimport { List, Avatar } from 'antd';\nimport styles from './index.less';\nimport { FormattedMessage } from 'react-intl';\nimport IconFont from '@/components/IconFont';\n\ninterface PageProps {\n  icon: string;\n  name: string;\n  remark?: string;\n  id: string;\n  onEditAccount: (id: string) => void;\n  onDeleteAccount: (id: string) => void;\n}\n\nexport default class Page extends React.Component<PageProps> {\n  handleEditAccount = () => {\n    const { onEditAccount, id } = this.props;\n    if (onEditAccount) {\n      onEditAccount(id);\n    }\n  };\n\n  handleDeleteAccount = () => {\n    const { onDeleteAccount, id } = this.props;\n    if (onDeleteAccount) {\n      onDeleteAccount(id);\n    }\n  };\n\n  render() {\n    const {\n      name,\n      remark = (\n        <FormattedMessage id=\"component.imagehostingListItem.noDescription\"></FormattedMessage>\n      ),\n      icon,\n    } = this.props;\n    let avatar;\n\n    if (icon.startsWith('http') || icon.indexOf('/') != -1) {\n      avatar = <Avatar src={icon} className={styles.avatar} />;\n    } else {\n      avatar = <IconFont type={icon} style={{ fontSize: 32 }} />;\n    }\n\n    const actions = [\n      <a key=\"edit\" onClick={this.handleEditAccount}>\n        <FormattedMessage id=\"component.imagehostingListItem.edit\" />\n      </a>,\n      <a key=\"delete\" onClick={this.handleDeleteAccount}>\n        <FormattedMessage id=\"component.imagehostingListItem.delete\" />\n      </a>,\n    ];\n\n    return (\n      <List.Item actions={actions}>\n        <List.Item.Meta avatar={avatar} title={<div>{name}</div>} description={remark} />\n      </List.Item>\n    );\n  }\n}\n"
  },
  {
    "path": "src/components/section/__snapshots__/index.test.tsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`test Section should render correct 1`] = `\ninitialize {\n  \"0\": Node {\n    \"attribs\": Object {},\n    \"children\": Array [\n      Node {\n        \"attribs\": Object {\n          \"class\": \"sectionTitle\",\n        },\n        \"children\": Array [\n          Node {\n            \"data\": \"test\",\n            \"next\": null,\n            \"parent\": [Circular],\n            \"prev\": null,\n            \"type\": \"text\",\n          },\n        ],\n        \"name\": \"h1\",\n        \"namespace\": \"http://www.w3.org/1999/xhtml\",\n        \"next\": Node {\n          \"attribs\": Object {},\n          \"children\": Array [\n            Node {\n              \"data\": \"test\",\n              \"next\": null,\n              \"parent\": [Circular],\n              \"prev\": null,\n              \"type\": \"text\",\n            },\n          ],\n          \"name\": \"div\",\n          \"namespace\": \"http://www.w3.org/1999/xhtml\",\n          \"next\": null,\n          \"parent\": [Circular],\n          \"prev\": [Circular],\n          \"type\": \"tag\",\n          \"x-attribsNamespace\": Object {},\n          \"x-attribsPrefix\": Object {},\n        },\n        \"parent\": [Circular],\n        \"prev\": null,\n        \"type\": \"tag\",\n        \"x-attribsNamespace\": Object {\n          \"class\": undefined,\n        },\n        \"x-attribsPrefix\": Object {\n          \"class\": undefined,\n        },\n      },\n      Node {\n        \"attribs\": Object {},\n        \"children\": Array [\n          Node {\n            \"data\": \"test\",\n            \"next\": null,\n            \"parent\": [Circular],\n            \"prev\": null,\n            \"type\": \"text\",\n          },\n        ],\n        \"name\": \"div\",\n        \"namespace\": \"http://www.w3.org/1999/xhtml\",\n        \"next\": null,\n        \"parent\": [Circular],\n        \"prev\": Node {\n          \"attribs\": Object {\n            \"class\": \"sectionTitle\",\n          },\n          \"children\": Array [\n            Node {\n              \"data\": \"test\",\n              \"next\": null,\n              \"parent\": [Circular],\n              \"prev\": null,\n              \"type\": \"text\",\n            },\n          ],\n          \"name\": \"h1\",\n          \"namespace\": \"http://www.w3.org/1999/xhtml\",\n          \"next\": [Circular],\n          \"parent\": [Circular],\n          \"prev\": null,\n          \"type\": \"tag\",\n          \"x-attribsNamespace\": Object {\n            \"class\": undefined,\n          },\n          \"x-attribsPrefix\": Object {\n            \"class\": undefined,\n          },\n        },\n        \"type\": \"tag\",\n        \"x-attribsNamespace\": Object {},\n        \"x-attribsPrefix\": Object {},\n      },\n    ],\n    \"name\": \"div\",\n    \"namespace\": \"http://www.w3.org/1999/xhtml\",\n    \"next\": null,\n    \"parent\": Node {\n      \"children\": Array [\n        [Circular],\n      ],\n      \"name\": \"root\",\n      \"next\": null,\n      \"parent\": null,\n      \"prev\": null,\n      \"type\": \"root\",\n    },\n    \"prev\": null,\n    \"type\": \"tag\",\n    \"x-attribsNamespace\": Object {},\n    \"x-attribsPrefix\": Object {},\n  },\n  \"_root\": [Circular],\n  \"length\": 1,\n  \"options\": Object {\n    \"decodeEntities\": true,\n    \"xml\": false,\n  },\n}\n`;\n"
  },
  {
    "path": "src/components/section/index.less",
    "content": ".sectionTitle {\n  color: rgba(0, 0, 0, 0.45);\n  line-height: 1.5;\n  font-size: 14px;\n  margin: 0 0 8px 0;\n}\n"
  },
  {
    "path": "src/components/section/index.tsx",
    "content": "import React from 'react';\nimport styles from './index.less';\n\ninterface Props {\n  title?: string | React.ReactNode;\n  className?: string;\n}\n\nconst Section: React.FC<Props> = ({ title, children, className }) => {\n  return (\n    <div className={className}>\n      {title && <h1 className={styles.sectionTitle}>{title}</h1>}\n      {children}\n    </div>\n  );\n};\nexport default Section;\n"
  },
  {
    "path": "src/components/share/index.tsx",
    "content": "import React from 'react';\nimport IconFont from '@/components/IconFont';\ninterface ShareProps {\n  content: string;\n}\nconst Share: React.FC<ShareProps> = ({ content: originContent }) => {\n  const content = encodeURIComponent(originContent.slice(0, 200));\n  const url = encodeURIComponent('https://clipper.website');\n\n  const twitterHref = `https://twitter.com/intent/tweet?via=yuanfandi&text=${content}&url=${url}`;\n  const weiboHref = `https://service.weibo.com/share/share.php?url=${url}&title=${content}&display=0`;\n  const doubanHref = `https://shuo.douban.com/!service/share?href=${url}&text=${content}`;\n\n  return (\n    <div style={{ fontSize: 20 }}>\n      <a target=\"_blank\" href={twitterHref}>\n        <IconFont type=\"twitter\" />\n      </a>\n      <a target=\"_blank\" href={weiboHref}>\n        <IconFont type=\"weibo\" style={{ marginLeft: 10 }} />\n      </a>\n      <a target=\"_blank\" href={doubanHref}>\n        <IconFont type=\"douban\" style={{ marginLeft: 10 }} />\n      </a>\n    </div>\n  );\n};\nexport default Share;\n"
  },
  {
    "path": "src/components/userItem/index.less",
    "content": ".userItem {\n  display: flex;\n  align-items: center;\n  color: #262626;\n  .userItemInfo {\n    margin-left: 8px;\n    align-self: flex-start;\n    color: #595959;\n  }\n  .description {\n    max-width: 120px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    color: #8c8c8c;\n  }\n}\n"
  },
  {
    "path": "src/components/userItem/index.tsx",
    "content": "import React from 'react';\nimport styles from './index.less';\nimport IconAvatar from '@/components/avatar';\n\ninterface UserItemProps {\n  avatar: string;\n  icon: string;\n  name: string;\n  description?: string;\n}\n\nconst UserItem: React.FC<UserItemProps> = props => (\n  <div className={styles.userItem}>\n    <IconAvatar size=\"large\" avatar={props.avatar} icon={props.icon} />\n    <div className={styles.userItemInfo}>\n      <div>{props.name}</div>\n      <div className={styles.description}>{props.description}</div>\n    </div>\n  </div>\n);\n\nexport default UserItem;\n"
  },
  {
    "path": "src/config.ts",
    "content": "interface WebClipperConfig {\n  icon: string;\n  iconDark: string;\n  yuqueClientId: string;\n  yuqueCallback: string;\n  yuqueScope: string;\n  oneNoteCallBack: string;\n  oneNoteClientId: string;\n}\n\nexport interface RemoteConfig {\n  iconfont: string;\n  chromeWebStoreVersion: string;\n}\n\nlet config: WebClipperConfig = {\n  icon: 'icons/icon.png',\n  iconDark: 'icons/icon-dark.png',\n  yuqueClientId: 'D1AwzCeDPLFWGfcGv7ze',\n  yuqueCallback: 'http://webclipper-oauth.yfd.im/yuque_oauth',\n  yuqueScope: 'doc,group,repo,attach_upload',\n  oneNoteClientId: '563571ad-cfcd-442a-aa34-046bad24b1b6',\n  oneNoteCallBack: 'https://webclipper-oauth.yfd.im/onenote_oauth',\n};\n\nif (process.env.NODE_ENV === 'development') {\n  config = Object.assign({}, config, {\n    icon: 'icons/icon-dev.png',\n  });\n}\n\nexport default config;\n"
  },
  {
    "path": "src/extensions/common.ts",
    "content": "import TurndownService from 'turndown';\nimport { IHighlighter } from '@web-clipper/highlight';\nimport { IAreaSelector } from '@web-clipper/area-selector';\nimport * as antd from 'antd';\nimport React from 'react';\nimport { IContentScriptService } from '@/service/common/contentScript';\nimport { IContextMenuExtension } from './contextMenus';\nimport { ISchema } from '@formily/react';\n\nexport interface InitContext {\n  accountInfo: {\n    type?: string;\n  };\n  url?: string;\n  locale: string;\n  pathname: string;\n  currentImageHostingService?: {\n    type: string;\n  };\n}\n\nexport interface ContentScriptContext {\n  $: JQueryStatic;\n  locale: string;\n  turndown: TurndownService;\n  Highlighter: Type<IHighlighter>;\n  AreaSelector: Type<IAreaSelector>;\n  toggleClipper: () => void;\n  toggleLoading: () => void;\n  Readability: any;\n  document: Document;\n  QRCode: any;\n}\n\nexport interface Message {\n  info(content: string): void;\n}\n\nexport interface UploadImageRequest {\n  data: string;\n}\n\nexport interface ImageHostingService {\n  getId(): string;\n\n  uploadImage(request: UploadImageRequest): Promise<string>;\n\n  uploadImageUrl(url: string): Promise<string>;\n}\n\ninterface CopyToClipboardOptions {\n  debug?: boolean;\n  message?: string;\n  format?: string; // MIME type\n}\n\nexport interface ToolContext<T, Out> {\n  locale: string;\n  result: T;\n  data: Out;\n  message: Message;\n  config?: any;\n  imageService?: ImageHostingService;\n  loadImage: any;\n  captureVisibleTab: any;\n  copyToClipboard: (text: string, options?: CopyToClipboardOptions) => void;\n  createAndDownloadFile: (fileName: string, content: string | Blob) => void;\n  pangu: (content: string) => Promise<string>;\n  antd: typeof antd;\n  React: typeof React;\n}\n\nexport interface IExtensionLifeCycle<T, U> {\n  /**\n   * 插件被加载之前\n   */\n  init?(context: InitContext): boolean;\n\n  /**\n   * 执行插件\n   */\n  run?(context: ContentScriptContext): Promise<T> | T;\n\n  /**\n   * 执行插件后\n   */\n  afterRun?(context: ToolContext<T, U>): Promise<U> | U;\n\n  /**\n   * 清理环境\n   */\n  destroy?(context: ContentScriptContext): void;\n}\n\nexport interface IExtensionManifest {\n  readonly extensionId?: string;\n  readonly name: string;\n\n  readonly version: string;\n\n  readonly description?: string;\n\n  readonly icon?: string;\n\n  readonly matches?: string[];\n\n  readonly apiVersion?: string;\n\n  readonly powerpack?: boolean;\n\n  readonly keywords?: string[];\n\n  readonly automatic?: boolean;\n\n  readonly config?: {\n    scheme: ISchema;\n    default: { [key: string]: string | string[] };\n  };\n\n  readonly i18nManifest?: {\n    [key: string]: {\n      readonly name?: string;\n      readonly description?: string;\n      readonly icon?: string;\n      readonly keywords?: string[];\n    };\n  };\n}\n\nexport enum ExtensionType {\n  Text = 'Text',\n  Image = 'Image',\n  Tool = 'tool',\n}\n\nexport interface SerializedExtension {\n  type: ExtensionType;\n  manifest: IExtensionManifest;\n  init?: string;\n  run?: string;\n  afterRun?: string;\n  destroy?: string;\n}\n\nexport interface SerializedExtensionWithId extends SerializedExtension {\n  id: string;\n  router: string;\n  embedded: boolean;\n}\n\nexport type SerializedExtensionInfo = Pick<SerializedExtensionWithId, 'type' | 'manifest' | 'id'>;\n\ninterface Type<T> extends Function {\n  new (...args: any[]): T;\n}\n\nexport interface IExtension<T, U> {\n  readonly type: ExtensionType;\n  readonly manifest: IExtensionManifest;\n  readonly extensionLifeCycle: IExtensionLifeCycle<T, U>;\n}\nexport interface IExtensionWithId<T = any, U = any> {\n  readonly id: string;\n  readonly router: string;\n  readonly type: ExtensionType;\n  readonly factory?: any;\n  readonly manifest: IExtensionManifest;\n  readonly extensionLifeCycle: IExtensionLifeCycle<T, U>;\n}\n\nclass AbstractExtension<T, U> {\n  public readonly type: ExtensionType;\n  public readonly manifest: IExtensionManifest;\n  public readonly extensionLifeCycle: IExtensionLifeCycle<T, U>;\n\n  constructor(\n    type: ExtensionType,\n    manifest: IExtensionManifest,\n    extensionLifeCycle: IExtensionLifeCycle<T, U>\n  ) {\n    this.type = type;\n    this.manifest = manifest;\n    this.extensionLifeCycle = extensionLifeCycle;\n  }\n}\n\nexport class TextExtension<T = string> extends AbstractExtension<T, string> {\n  constructor(manifest: IExtensionManifest, methods: IExtensionLifeCycle<T, string>) {\n    super(ExtensionType.Text, manifest, methods);\n  }\n}\n\nexport class ToolExtension<T = string> extends AbstractExtension<T, string> {\n  constructor(manifest: IExtensionManifest, methods: IExtensionLifeCycle<T, string>) {\n    super(ExtensionType.Tool, manifest, methods);\n  }\n}\n\nexport interface IContextMenuContext {\n  contentScriptService: IContentScriptService;\n  initContentScriptService(id: number): Promise<void>;\n}\n\nexport interface IContextMenuExtensionFactory {\n  id: string;\n  new (): IContextMenuExtension;\n}\n\nexport interface IContextMenusWithId {\n  id: string;\n  contextMenu: IContextMenuExtensionFactory;\n}\n\nexport function getLocaleExtensionManifest(manifest: IExtensionManifest, locale: string) {\n  const { i18nManifest, ...rest } = manifest;\n  let localeManifest = {};\n  if (i18nManifest && typeof i18nManifest === 'object') {\n    localeManifest = i18nManifest[locale];\n  }\n  return {\n    ...rest,\n    ...localeManifest,\n  };\n}\n"
  },
  {
    "path": "src/extensions/contextMenus/saveSelection/saveSelection.ts",
    "content": "import { ContextMenuExtension, IContextMenuContext } from '../../contextMenus';\nimport localeService from '@/common/locales';\nimport { stringify } from 'qs';\n\nclass ContextMenu extends ContextMenuExtension {\n  static id = 'contextMenus.selection.save';\n\n  constructor() {\n    super({\n      extensionId: 'contextMenus.selection.save',\n      name: `${localeService.format({\n        id: 'contextMenus.selection.save.title',\n      })} (Alt+S)`,\n      description: localeService.format({\n        id: 'contextMenus.selection.save.description',\n      }),\n      config: {\n        scheme: {\n          type: 'object',\n          properties: {\n            template: {\n              type: 'string',\n              title: localeService.format({\n                id: 'extension.link.config.template',\n              }),\n              'x-decorator': 'FormItem',\n              'x-component': 'textarea',\n              'x-component-props': { autoSize: true },\n            },\n          },\n        },\n        default: {\n          template: localeService.getMessage('contextMenus.selection.save.template'),\n        },\n      },\n      version: '0.0.1',\n      contexts: ['selection'],\n    });\n  }\n\n  async run(tab: chrome.tabs.Tab, context: IContextMenuContext): Promise<void> {\n    // await context.initContentScriptService(tab.id!);\n    const content = await context.contentScriptService.getSelectionMarkdown();\n    const config = (await context.config!) as { template: string };\n    const note = localeService.format(\n      {\n        id: 'not_exist',\n        defaultMessage: config.template,\n      },\n      { CONTENT: content, URL: await context.contentScriptService.getPageUrl(), TITLE: tab.title }\n    );\n    context.contentScriptService.toggle({\n      pathname: '/editor',\n      query: stringify({ markdown: note }),\n    });\n  }\n}\n\nexport default ContextMenu;\n"
  },
  {
    "path": "src/extensions/contextMenus.ts",
    "content": "import { IContentScriptService } from '@/service/common/contentScript';\nimport { IExtensionManifest } from './common';\n\nexport interface IContextMenuProperties {\n  id: string;\n  title: string;\n  contexts: string[];\n}\n\ninterface IContextMenuExtensionManifest extends IExtensionManifest {\n  contexts?: string[];\n}\n\nexport interface IContextMenuExtension {\n  readonly manifest: IContextMenuExtensionManifest;\n  run(id: chrome.tabs.Tab, context: IContextMenuContext): Promise<void>;\n}\n\nexport interface IContextMenuContext {\n  config: unknown;\n  contentScriptService: IContentScriptService;\n  // initContentScriptService(id: number): Promise<void>;\n}\n\nexport abstract class ContextMenuExtension implements IContextMenuExtension {\n  constructor(public manifest: IContextMenuExtensionManifest) {}\n\n  abstract run(id: chrome.tabs.Tab, context: IContextMenuContext): Promise<void>;\n}\n\nexport interface IContextMenuExtensionFactory {\n  id: string;\n  new (): IContextMenuExtension;\n}\n\nexport interface IContextMenusWithId {\n  id: string;\n  contextMenu: IContextMenuExtensionFactory;\n}\n"
  },
  {
    "path": "src/extensions/extensions/bookmark.ts",
    "content": "import { TextExtension } from '@/extensions/common';\n\nexport default new TextExtension(\n  {\n    name: 'Bookmark',\n    version: '0.0.1',\n    description: 'Add bookmark.',\n    icon: 'link',\n    i18nManifest: {\n\t\t\t'de-DE': { name: 'Lesezeichen', description: 'Lesezeichen hinzufügen.' },\n\t\t\t'en-US': { name: 'Bookmark', description: 'Add bookmark.' },\n\t\t\t'ja-JP': { name: 'ブックマーク', description: 'ブックマークを追加します。' },\n\t\t\t'ko-KR': { name: '북마크', description: '북마크 추가.' },\n\t\t\t'ru-RU': { name: 'Закладка', description: 'Добавить закладку.' },\n\t\t\t'zh-CN': { name: '书签', description: '添加书签' },\n    },\n  },\n  {\n    run: async (context) => {\n      const { document, locale } = context;\n      switch (locale) {\n        case 'zh-CN': {\n          return `## 链接 \\n [${document.URL}](${document.URL}) \\n ## 备注:`;\n        }\n        default:\n          return `## Link \\n [${document.URL}](${document.URL}) \\n ## Comment:`;\n      }\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/extensions/remove.ts",
    "content": "import { ToolExtension } from '@/extensions/common';\n\nexport default new ToolExtension(\n  {\n    name: 'Delete Element',\n    icon: 'delete',\n    version: '0.0.1',\n    description: 'Delete selected page elements.',\n    i18nManifest: {\n      'de-DE': { name: 'Element löschen', description: 'Ausgewählte Seitenelemente löschen.' },\n      'en-US': { name: 'Delete Element', description: 'Delete selected page elements.' },\n      'ja-JP': { name: '要素を削除', description: '選択したページ要素を削除します。' },\n      'ko-KR': { name: '요소 삭제', description: '선택한 페이지 요소를 삭제합니다.' },\n      'ru-RU': { name: 'Удалить элемент', description: 'Удалить выбранные элементы страницы.' },\n      'zh-CN': { name: '删除元素', description: '删除选择的页面元素。' },\n      'zh-TW': { name: '刪除元素', description: '刪除選擇的頁面元素。' },\n    },\n  },\n  {\n    run: async context => {\n      const { $, Highlighter, toggleClipper } = context;\n      toggleClipper();\n      const data = await new Highlighter().start();\n      $(data).remove();\n      toggleClipper();\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/extensions/selectTool.ts",
    "content": "import { ToolExtension } from '@/extensions/common';\n\nexport default new ToolExtension(\n  {\n    name: 'Manual selection',\n    icon: 'select',\n    version: '0.0.1',\n    description: 'Manual selection page element.',\n    i18nManifest: {\n      'de-DE': { name: 'Manuelle Auswahl', description: 'Manuelle Auswahl von Seitenelementen.' },\n      'en-US': { name: 'Manual selection', description: 'Manual selection of page elements.' },\n      'ja-JP': { name: '手動選択', description: 'ページ要素を手動で選択します。' },\n      'ko-KR': { name: '수동 선택', description: '페이지 요소를 수동으로 선택합니다.' },\n      'ru-RU': { name: 'Ручной выбор', description: 'Ручной выбор элементов страницы.' },\n      'zh-CN': { name: '手动选取', description: '手动选取页面中的元素' },\n    },\n  },\n  {\n    init: ({ pathname }) => {\n      if (pathname === '/') {\n        return false;\n      }\n      return true;\n    },\n    run: async context => {\n      const { turndown, Highlighter, toggleClipper } = context;\n      toggleClipper();\n      try {\n        const data = await new Highlighter().start();\n        return turndown.turndown(data);\n      } catch (error) {\n        throw error;\n      } finally {\n        toggleClipper();\n      }\n    },\n    afterRun: context => {\n      const { result, data } = context;\n      return `${data}\\n${result}`;\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/extensions/uploadImage.ts",
    "content": "import { ToolExtension } from '@/extensions/common';\n\nexport default new ToolExtension(\n  {\n    name: 'Upload Image',\n    icon: 'sync',\n    version: '0.0.1',\n    automatic: true,\n    description: 'Upload images to image host.',\n    i18nManifest: {\n      'de-DE': { name: 'Bild hochladen', description: 'Bilder auf den Bildhost hochladen.' },\n      'en-US': { name: 'Upload Image', description: 'Upload images to image host.' },\n      'ja-JP': { name: '画像をアップロード', description: '画像を画像ホストにアップロードします。' },\n      'ko-KR': { name: '이미지 업로드', description: '이미지를 이미지 호스트에 업로드합니다.' },\n      'ru-RU': { name: 'Загрузить изображение', description: 'Загрузить изображения на хост изображений.' },\n      'zh-CN': { name: '上传图片', description: '把文章内图片上传到图床' },\n    },\n  },\n  {\n    init: ({ pathname, currentImageHostingService }) =>\n      pathname.startsWith('/plugins') && !!currentImageHostingService,\n    afterRun: async context => {\n      const { data, imageService, message } = context;\n      let foo = data;\n      const result = data.match(/!\\[.*?\\]\\(http(.*?)\\)/g);\n      let successCount = 0;\n      let failedCount = 0;\n      if (result) {\n        const images: string[] = result\n          .map(o => {\n            const temp = /!\\[.*?\\]\\((http.*?)\\)/.exec(o);\n            if (temp) {\n              return temp[1];\n            }\n            return '';\n          })\n          .filter(o => o && !o.startsWith('https://cdn-pri.nlark.com'));\n\n        for (let image of images) {\n          try {\n            const url = await imageService!.uploadImageUrl(image);\n            foo = foo.replace(image, url);\n            successCount++;\n          } catch (_error) {\n            failedCount++;\n          }\n        }\n      }\n      message.info(`${successCount} success,${failedCount} failed.`);\n      return foo;\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/fullPage.ts",
    "content": "import { TextExtension } from '@/extensions/common';\n\nexport default new TextExtension(\n  {\n    name: 'Full Page',\n    version: '0.0.1',\n    description: 'Save Full Page and turn ro Markdown.',\n    icon: 'copy',\n\t\ti18nManifest: {\n\t\t\t'de-DE': { name: 'Vollständige Seite', description: 'Speichern Sie die gesamte Seite und konvertieren Sie sie in Markdown.' },\n\t\t\t'en-US': { name: 'Full Page', description: 'Save Full Page and turn to Markdown.' },\n\t\t\t'ja-JP': { name: '全ページ', description: '全ページを保存し、Markdownに変換します。' },\n\t\t\t'ko-KR': { name: '전체 페이지', description: '전체 페이지를 저장하고 Markdown으로 변환합니다.' },\n\t\t\t'ru-RU': { name: 'Полная страница', description: 'Сохранить полную страницу и преобразовать в Markdown.' },\n\t\t\t'zh-CN': { name: '整个页面', description: '把整个页面元素转换为 Markdown' },\n\t\t}\n\t},\n  {\n    run: async context => {\n      const { turndown, $ } = context;\n      const $body = $('html').clone();\n      $body.find('script').remove();\n      $body.find('style').remove();\n      $body.removeClass();\n      return turndown.turndown($body.html());\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/qrcode.ts",
    "content": "import { TextExtension } from '@/extensions/common';\n\nexport default new TextExtension<string>(\n  {\n    name: 'QR code',\n    icon: 'qrcode',\n    version: '0.0.1',\n    description: 'Convert the URL of the current page to a QR code.',\n\t\ti18nManifest: {\n\t\t\t'de-DE': { name: 'QR-Code', description: 'Konvertieren Sie die URL der aktuellen Seite in einen QR-Code.' },\n\t\t\t'en-US': { name: 'QR code', description: 'Convert the URL of the current page to a QR code.' },\n\t\t\t'ja-JP': { name: 'QRコード', description: '現在のページのURLをQRコードに変換します。' },\n\t\t\t'ko-KR': { name: 'QR 코드', description: '현재 페이지의 URL을 QR 코드로 변환합니다.' },\n\t\t\t'ru-RU': { name: 'QR код', description: 'Преобразовать URL текущей страницы в QR-код.' },\n\t\t\t'zh-CN': { name: '二维码', description: '显示当前链接为二维码' },\n\t\t}\n\t},\n  {\n    init: ({ currentImageHostingService }) => !!currentImageHostingService,\n    run: async context => {\n      const { QRCode, document } = context;\n      const dataUrl = await QRCode.toDataURL(document.URL);\n      return dataUrl;\n    },\n    afterRun: async context => {\n      const { result: dataUrl } = context;\n      return `![](${dataUrl})\\n`;\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/readability.ts",
    "content": "import { TextExtension } from '@/extensions/common';\n\nexport default new TextExtension(\n  {\n    name: 'Readability',\n    icon: 'copy',\n    version: '0.0.1',\n    description: 'Intelligent extraction of webpage main content.',\n\t\ti18nManifest: {\n\t\t\t'de-DE': { name: 'Lesbarkeit', description: 'Intelligente Extraktion des Hauptinhalts der Webseite.' },\n\t\t\t'en-US': { name: 'Readability', description: 'Intelligent extraction of webpage main content.' },\n\t\t\t'ja-JP': { name: '読みやすさ', description: 'ウェブページの主要な内容をインテリジェントに抽出します。' },\n\t\t\t'ko-KR': { name: '가독성', description: '웹 페이지의 주요 내용을 지능적으로 추출합니다.' },\n\t\t\t'ru-RU': { name: 'Читаемость', description: 'Интеллектуальная извлечение основного содержимого веб-страницы.' },\n\t\t\t'zh-CN': { name: '智能提取', description: '智能提取当前页面元素' },\n\t\t}\n\t},\n  {\n    run: async context => {\n      const { turndown, document, Readability, $ } = context;\n      let documentClone = document.cloneNode(true);\n      $(documentClone)\n        .find('#skPlayer')\n        .remove();\n      let article = new Readability(documentClone, {\n        keepClasses: true,\n      }).parse();\n      return turndown.turndown(article.content);\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/screenshot.ts",
    "content": "import { TextExtension } from '@/extensions/common';\nimport { SelectAreaPosition } from '@web-clipper/area-selector';\n\nexport default new TextExtension<SelectAreaPosition>(\n  {\n    name: 'Screenshots',\n    icon: 'picture',\n    version: '0.0.1',\n\t\ti18nManifest: {\n\t\t\t'de-DE': { name: 'Screenshots', description: 'Speichern Sie den aktuellen Inhalt als Bild.' },\n\t\t\t'en-US': { name: 'Screenshots', description: 'Save current clipping content as an image.' },\n\t\t\t'ja-JP': { name: 'スクリーンショット', description: '現在のクリップ内容を画像として保存します。' },\n\t\t\t'ko-KR': { name: '스크린샷', description: '현재 클립 내용을 이미지로 저장합니다.' },\n\t\t\t'ru-RU': { name: 'Скриншоты', description: 'Сохранить текущее содержимое как изображение.' },\n\t\t\t'zh-CN': { name: '截图', description: '将当前剪藏内容保存为图片' }, // 保留简体中文\n\t\t}\n\t},\n  {\n    init: ({ currentImageHostingService }) => !!currentImageHostingService,\n    run: async context => {\n      const { AreaSelector, toggleClipper, toggleLoading } = context;\n      toggleClipper();\n      const response = await new AreaSelector().start();\n      toggleLoading();\n      return response;\n    },\n    afterRun: async context => {\n      const { result, loadImage, captureVisibleTab, imageService } = context;\n      const base64Capture = await captureVisibleTab();\n      const img = await loadImage(base64Capture);\n      let canvas: HTMLCanvasElement = document.createElement('canvas');\n      let ctx = canvas.getContext('2d');\n      let sx;\n      let sy;\n      let sheight;\n      let swidth;\n      let {\n        rightBottom: { clientX: rightBottomX, clientY: rightBottomY },\n        leftTop: { clientX: leftTopX, clientY: leftTopY },\n      } = result;\n      if (rightBottomX === leftTopX && rightBottomY === leftTopY) {\n        sx = 0;\n        sy = 0;\n        swidth = img.width;\n        sheight = img.height;\n      } else {\n        const dpi = window.devicePixelRatio;\n        sx = leftTopX * dpi;\n        sy = leftTopY * dpi;\n        swidth = (rightBottomX - leftTopX) * dpi;\n        sheight = (rightBottomY - leftTopY) * dpi;\n      }\n      canvas.height = sheight;\n      canvas.width = swidth;\n      ctx!.drawImage(img, sx, sy, swidth, sheight, 0, 0, swidth, sheight);\n      const url = await imageService!.uploadImage({\n        data: canvas.toDataURL(),\n      });\n      return `![](${url})\\n\\n`;\n    },\n    destroy: async context => {\n      const { toggleClipper, toggleLoading } = context;\n      toggleLoading();\n      toggleClipper();\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/select.ts",
    "content": "import { TextExtension } from '@/extensions/common';\n\nexport default new TextExtension(\n  {\n    name: 'Manual selection',\n    icon: 'select',\n    version: '0.0.1',\n    description: 'Manual selection page element.',\n\t\ti18nManifest: {\n\t\t\t'de-DE': { name: 'Manuelle Auswahl', description: 'Manuelle Auswahl von Seitenelementen.' },\n\t\t\t'en-US': { name: 'Manual selection', description: 'Manual selection of page elements.' },\n\t\t\t'ja-JP': { name: '手動選択', description: 'ページ要素を手動で選択します。' },\n\t\t\t'ko-KR': { name: '수동 선택', description: '페이지 요소를 수동으로 선택합니다.' },\n\t\t\t'ru-RU': { name: 'Ручной выбор', description: 'Ручной выбор элементов страницы.' },\n\t\t\t'zh-CN': { name: '手动选取', description: '手动选取页面元素' },\n\t\t}\n\t},\n  {\n    run: async context => {\n      const { turndown, Highlighter, toggleClipper, $ } = context;\n      toggleClipper();\n      try {\n        const data = await new Highlighter().start();\n        let container = document.createElement('div');\n        container.appendChild(\n          $(data)\n            .clone()\n            .get(0)\n        );\n        return turndown.turndown(container);\n      } catch (error) {\n        throw error;\n      } finally {\n        toggleClipper();\n      }\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/web-clipper/clear.ts",
    "content": "import { ToolExtension } from '@/extensions/common';\n\nexport default new ToolExtension(\n  {\n    name: 'Clear',\n    icon: 'close-circle',\n    version: '0.0.1',\n    description: 'Clear Content',\n    apiVersion: '1.12.0',\n\t\ti18nManifest: {\n\t\t\t'de-DE': { name: 'Löschen', description: 'Inhalt löschen.' },\n\t\t\t'en-US': { name: 'Clear', description: 'Clear Content' },\n\t\t\t'ja-JP': { name: 'クリア', description: '内容をクリアします。' },\n\t\t\t'ko-KR': { name: '지우기', description: '내용 지우기' },\n\t\t\t'ru-RU': { name: 'Очистить', description: 'Очистить содержимое' },\n\t\t\t'zh-CN': { name: '清空', description: '清空内容' },\n\t\t}\n\t},\n  {\n    init: ({ pathname }) => {\n      return pathname.startsWith('/plugin');\n    },\n    afterRun: () => {\n      return '';\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/web-clipper/copyToClipboard.ts",
    "content": "import { ToolExtension } from '@/extensions/common';\n\nexport default new ToolExtension(\n  {\n    name: 'Copy To Clipboard',\n    icon: 'copy',\n    version: '0.0.1',\n    description: 'Copy To Clipboard',\n\t\ti18nManifest: {\n\t\t\t'de-DE': { name: 'In die Zwischenablage kopieren', description: 'In die Zwischenablage kopieren.' },\n\t\t\t'en-US': { name: 'Copy To Clipboard', description: 'Copy To Clipboard' },\n\t\t\t'ja-JP': { name: 'クリップボードにコピー', description: 'クリップボードにコピーします。' },\n\t\t\t'ko-KR': { name: '클립보드에 복사', description: '클립보드에 복사합니다.' },\n\t\t\t'ru-RU': { name: 'Копировать в буфер обмена', description: 'Копировать в буфер обмена' },\n\t\t\t'zh-CN': { name: '复制', description: '复制到剪贴板' },\n\t\t}\n\t},\n  {\n    afterRun: ({ copyToClipboard, data }) => {\n      copyToClipboard(data);\n      return data;\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/web-clipper/download.ts",
    "content": "import { ToolExtension } from '@/extensions/common';\n\nexport default new ToolExtension(\n  {\n    name: 'Save as Markdown',\n    icon: 'file-markdown',\n    version: '0.0.2',\n    description: 'Save as Markdown and Download.',\n    apiVersion: '1.12.0',\n    i18nManifest: {\n      'de-DE': { name: 'Als Markdown speichern', description: 'Als Markdown speichern und herunterladen.' },\n      'en-US': { name: 'Save as Markdown', description: 'Save as Markdown and Download.' },\n      'ja-JP': { name: 'Markdownとして保存', description: 'Markdownとして保存し、ダウンロードします。' },\n      'ko-KR': { name: 'Markdown으로 저장', description: 'Markdown으로 저장하고 다운로드합니다.' },\n      'ru-RU': { name: 'Сохранить как Markdown', description: 'Сохранить как Markdown и скачать.' },\n      'zh-CN': { name: '保存为 Markdown', description: '保存为 Markdown 并下载' },\n    },\n\t},\n  {\n    init: ({ pathname }) => {\n      return pathname.startsWith('/plugin');\n    },\n    run: ({ document }) => {\n      return document.title;\n    },\n    afterRun: ({ createAndDownloadFile, data, result }) => {\n      createAndDownloadFile(`${result || 'content'}.md`, data);\n      return data;\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/extensions/web-clipper/link.tsx",
    "content": "import { ToolExtension } from '@/extensions/common';\nimport localeService from '@/common/locales';\n\nexport default class Link extends ToolExtension<any> {\n  constructor() {\n    super(\n      {\n        extensionId: 'link',\n        name: 'Link',\n        icon: 'link',\n        version: '0.0.2',\n        automatic: true,\n        description: 'Add link at the end of the document.',\n        config: {\n          scheme: {\n            type: 'object',\n            properties: {\n              template: {\n                type: 'string',\n                title: localeService.format({\n                  id: 'extension.link.config.template',\n                }),\n                'x-decorator': 'FormItem',\n                'x-component': 'textarea',\n                'x-component-props': { autoSize: true },\n              },\n              autoRunExclude: {\n                type: 'array',\n                title: localeService.format({\n                  id: 'extension.link.config.autoRunExclude',\n                }),\n                'x-decorator': 'FormItem',\n                'x-component': 'clipExtensionsSelect',\n              },\n            },\n          },\n          default: {\n            template: '[{TITLE}]({URL}) \\n\\n {DOCUMENT}',\n            autoRunExclude: [],\n          },\n        },\n\t\t\t\ti18nManifest: {\n\t\t\t\t\t'de-DE': { name: 'Link', description: 'Fügen Sie am Ende des Dokuments einen Link hinzu.' },\n\t\t\t\t\t'en-US': { name: 'Link', description: 'Add link at the end of the document.' },\n\t\t\t\t\t'ja-JP': { name: 'リンク', description: 'ドキュメントの最後にリンクを追加します。' },\n\t\t\t\t\t'ko-KR': { name: '링크', description: '문서 끝에 링크를 추가합니다.' },\n\t\t\t\t\t'ru-RU': { name: 'Ссылка', description: 'Добавить ссылку в конце документа.' },\n\t\t\t\t\t'zh-CN': { name: '添加模版', description: '根据插件设置中的模板，添加内容，默认添加页面链接' },\n\t\t\t\t}\n\t\t\t},\n      {\n        run: async context => {\n          return {\n            TITLE: context.document.title,\n            URL: context.document.URL,\n          };\n        },\n        afterRun: async context => {\n          const config: { template: string } = context.config!;\n          return localeService.format(\n            { id: 'plugin.link', defaultMessage: config.template },\n            { DOCUMENT: context.data, TITLE: context.result.TITLE, URL: context.result.URL }\n          );\n        },\n      }\n    );\n  }\n}\n"
  },
  {
    "path": "src/extensions/extensions/web-clipper/pangu.ts",
    "content": "import { ToolExtension } from '@/extensions/common';\nimport { SelectAreaPosition } from '@web-clipper/area-selector';\n\nexport default new ToolExtension<SelectAreaPosition>(\n  {\n    name: 'Pangu',\n    icon: 'pangu',\n    version: '0.0.2',\n    automatic: true,\n    apiVersion: '1.13.0',\n    description: 'Paranoid text spacing in JavaScript',\n    powerpack: false,\n    i18nManifest: {\n      'de-DE': { name: 'Pangu', description: 'Fügen Sie Leerzeichen zwischen chinesischen und englischen Zeichen ein.' },\n      'en-US': { name: 'Pangu', description: 'Paranoid text spacing in JavaScript' },\n      'ja-JP': { name: 'Pangu', description: 'すべての中国語と半角英数字、記号の間に空白を挿入します。' },\n      'ko-KR': { name: 'Pangu', description: '모든 한자와 반각 영어, 숫자, 기호 사이에 공백을 삽입합니다.' },\n      'ru-RU': { name: 'Pangu', description: 'Вставка пробелов между китайскими и английскими символами.' },\n      'zh-CN': { name: 'Pangu', description: '所有的中文字和半形的英文、数字、符号之间插入空白。' },\n    },\n  },\n  {\n    afterRun: async context => {\n      const { pangu, data } = context;\n      return pangu(data);\n    },\n  }\n);\n"
  },
  {
    "path": "src/extensions/index.ts",
    "content": "import { IExtensionWithId, ToolExtension, TextExtension } from './common';\nimport { IContextMenuExtensionFactory } from './contextMenus';\n\nconst context = require.context('./extensions', true, /\\.(ts|tsx)$/);\n\nconst contextMenusContext = require.context('./contextMenus', true, /\\.(ts|tsx)$/);\n\nexport const contextMenus = contextMenusContext.keys().map(key => {\n  const ContextMenuExtensionFactory: IContextMenuExtensionFactory = contextMenusContext(key)\n    .default;\n  return {\n    id: ContextMenuExtensionFactory.id,\n    contextMenu: ContextMenuExtensionFactory,\n  };\n});\n\nexport const extensions: IExtensionWithId[] = context.keys().map(key => {\n  const id = key.slice(2, key.length - 3);\n  const extension = context(key).default;\n  if (extension instanceof ToolExtension || extension instanceof TextExtension) {\n    return {\n      ...context(key).default,\n      id,\n      router: `/plugins/${id}`,\n    };\n  }\n  return {\n    factory: extension,\n    id,\n    router: `/plugins/${id}`,\n  };\n});\n"
  },
  {
    "path": "src/hooks/useOriginForm.tsx",
    "content": "import React from 'react';\nimport { FormattedMessage } from 'react-intl';\nimport useOriginPermission from '@/common/hooks/useOriginPermission';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\n\ninterface UseOriginFormProps extends FormComponentProps {\n  initStatus: boolean;\n  originKey?: string;\n}\n\nconst useOriginForm = ({ initStatus, form, originKey }: UseOriginFormProps) => {\n  const key = originKey || 'origin';\n  const [verified, requestOriginPermission] = useOriginPermission(initStatus);\n  const handleAuthentication = () => {\n    form.validateFields([key], async (err, value) => {\n      if (err) {\n        return;\n      }\n      requestOriginPermission(value[key]);\n    });\n  };\n  const formRules = [\n    {\n      required: true,\n      message: (\n        <FormattedMessage\n          id=\"hooks.useOriginForm.origin.message\"\n          defaultMessage={`Wrong format,Examples https://developer.mozilla.org`}\n        />\n      ),\n    },\n    {\n      validator(_r: any, value: string, callback: Function) {\n        if (!value) {\n          return callback();\n        }\n        try {\n          const _url = new URL(value);\n          if (_url.origin !== value) {\n            form.setFieldsValue({\n              [key]: _url.toString(),\n            });\n            callback();\n          }\n          callback();\n        } catch (_error) {\n          return callback(\n            <FormattedMessage\n              id=\"hooks.useOriginForm.origin.message\"\n              defaultMessage={`Wrong format,Examples https://developer.mozilla.org`}\n            />\n          );\n        }\n      },\n    },\n  ];\n  return { verified, handleAuthentication, formRules };\n};\n\nexport default useOriginForm;\n"
  },
  {
    "path": "src/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>webpack App</title>\n    <style>\n      .web-clipper-loading {\n        position: fixed;\n        right: 10px;\n        top: 10px;\n        box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px;\n        background: white;\n        width: 324px;\n        height: 150px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n      }\n\n      .web-clipper-loading .line {\n        animation: expand 1s ease-in-out infinite;\n        border-radius: 10px;\n        display: inline-block;\n        transform-origin: center center;\n        margin: 0 3px;\n        width: 1px;\n        height: 25px;\n      }\n\n      .web-clipper-loading .line:nth-child(1) {\n        background: #27ae60;\n      }\n\n      .web-clipper-loading .line:nth-child(2) {\n        animation-delay: 180ms;\n        background: #f1c40f;\n      }\n\n      .web-clipper-loading .line:nth-child(3) {\n        animation-delay: 360ms;\n        background: #e67e22;\n      }\n\n      .web-clipper-loading .line:nth-child(4) {\n        animation-delay: 540ms;\n        background: #2980b9;\n      }\n\n      @keyframes expand {\n        0% {\n          transform: scale(1);\n        }\n        25% {\n          transform: scale(2);\n        }\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"app\">\n      <div class=\"web-clipper-loading\">\n        <div>\n          <div class=\"line\"></div>\n          <div class=\"line\"></div>\n          <div class=\"line\"></div>\n          <div class=\"line\"></div>\n        </div>\n      </div>\n    </div>\n    <script type=\"text/javascript\" src=\"vendor.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/main/background.worker.ts",
    "content": "import 'reflect-metadata';\nimport 'regenerator-runtime/runtime';\n\n// services\nimport { IContentScriptService } from '@/service/common/contentScript';\nimport { ICookieService } from '@/service/common/cookie';\nimport { IChannelServer } from '@/service/common/ipc';\nimport { IPermissionsService } from '@/service/common/permissions';\nimport { ITabService } from '@/service/common/tab';\nimport { IWebRequestService } from '@/service/common/webRequest';\nimport { ContentScriptChannelClient } from '@/service/contentScript/common/contentScriptIPC';\nimport '@/service/cookie/background/cookieService';\nimport { CookieChannel } from '@/service/cookie/common/cookieIpc';\nimport { BackgroundIPCServer } from '@/service/ipc/browser/background-main/ipcService';\nimport { PopupContentScriptIPCClient } from '@/service/ipc/browser/popup/ipcClient';\nimport '@/service/permissions/chrome/permissionsService';\nimport { PermissionsChannel } from '@/service/permissions/common/permissionsIpc';\nimport '@/service/tab/browser/background/tabService';\nimport { TabChannel } from '@/service/tab/common/tabIpc';\nimport '@/service/webRequest/chrome/background/tabService';\nimport { WebRequestChannel } from '@/service/webRequest/common/webRequestIPC';\nimport Container from 'typedi';\nimport { WorkerServiceChannel } from '@/service/worker/common/workserServiceIPC';\nimport '@/service/worker/worker/workerService';\nimport { IWorkerService } from '@/service/worker/common';\nimport '@/service/extension/browser/extensionContainer';\nimport '@/service/extension/browser/extensionService';\nimport { ILocalStorageService } from '@/service/common/storage';\n//\nimport { syncStorageService, localStorageService } from '@/common/chrome/storage';\nContainer.set(ILocalStorageService, localStorageService);\nContainer.set(ISyncStorageService, syncStorageService);\nimport { ISyncStorageService } from '@/service/common/storage';\n//\nimport localeService from '@/common/locales';\nimport { ILocaleService } from '@/service/common/locale';\nimport { IExtensionContainer, IExtensionService } from '@/service/common/extension';\nimport { getResourcePath } from '@/common/getResource';\nContainer.set(ILocaleService, localeService);\n\nfunction main() {\n  const backgroundIPCServer: IChannelServer = new BackgroundIPCServer();\n  backgroundIPCServer.registerChannel('tab', new TabChannel(Container.get(ITabService)));\n  backgroundIPCServer.registerChannel(\n    'worker',\n    new WorkerServiceChannel(Container.get(IWorkerService))\n  );\n  const contentScriptIPCClient = new PopupContentScriptIPCClient(Container.get(ITabService));\n  const contentScriptChannel = contentScriptIPCClient.getChannel('contentScript');\n  Container.set(IContentScriptService, new ContentScriptChannelClient(contentScriptChannel));\n  const contentScriptService = Container.get(IContentScriptService);\n  chrome.action.onClicked.addListener((tab) => {\n    if (!tab || !tab.id) {\n      return;\n    }\n    contentScriptService\n      .checkStatus()\n      .then(() => {\n        contentScriptService.toggle();\n      })\n      .catch((e) => {\n        chrome.tabs.create({\n          url: `${chrome.runtime.getURL(getResourcePath('error.html'))}?message=${e.message}`,\n        });\n      });\n  });\n  backgroundIPCServer.registerChannel(\n    'permissions',\n    new PermissionsChannel(Container.get(IPermissionsService))\n  );\n\n  backgroundIPCServer.registerChannel(\n    'webRequest',\n    new WebRequestChannel(Container.get(IWebRequestService))\n  );\n\n  backgroundIPCServer.registerChannel('cookies', new CookieChannel(Container.get(ICookieService)));\n\n  chrome.contextMenus.onClicked.addListener(async (_info, tab) => {\n    const extensionContainer = Container.get(IExtensionContainer);\n    const extensionService = Container.get(IExtensionService);\n    const contentScriptService = Container.get(IContentScriptService);\n    await extensionContainer.init();\n    await extensionService.init();\n    const contextMenus = extensionContainer.contextMenus;\n    const currentContextMenus = contextMenus.filter(\n      (p) => !extensionService.DisabledExtensionIds.includes(p.id)\n    );\n    let config: unknown;\n    const Menu = currentContextMenus.find((p) => p.id === _info.menuItemId)!;\n    if (!Menu) {\n      return;\n    }\n    const instance = new Menu.contextMenu();\n    if (instance.manifest.extensionId) {\n      config =\n        extensionService.getExtensionConfig(instance.manifest.extensionId!) ||\n        instance.manifest.config?.default;\n    }\n    instance.run(tab!, {\n      config,\n      contentScriptService,\n    });\n  });\n\n  chrome.commands.onCommand.addListener(async (e) => {\n    if (e === 'save-selection') {\n      const extensionService = Container.get(IExtensionService);\n      const extensionContainer = Container.get(IExtensionContainer);\n      const contextMenus = extensionContainer.contextMenus;\n      const currentContextMenus = contextMenus.filter(\n        // eslint-disable-next-line max-nested-callbacks\n        (p) => !extensionService.DisabledExtensionIds.includes(p.id)\n      );\n      for (const iterator of currentContextMenus) {\n        const Factory = iterator.contextMenu;\n        const instance = new Factory();\n        if (iterator.id === 'contextMenus.selection.save') {\n          let config: unknown;\n          if (instance.manifest.extensionId) {\n            config =\n              extensionService.getExtensionConfig(instance.manifest.extensionId!) ||\n              instance.manifest.config?.default;\n          }\n          instance.run((await Container.get(ITabService).getCurrent()) as any, {\n            config,\n            contentScriptService,\n          });\n        }\n      }\n    }\n  });\n}\n\ntry {\n  main();\n} catch (error) {\n  console.log((error as Error).message);\n  console.error(error);\n}\n"
  },
  {
    "path": "src/main/contentScript.main.ts",
    "content": "import 'reflect-metadata';\nimport 'regenerator-runtime/runtime';\n\n//\n\nimport { localStorageService, syncStorageService } from '@/common/chrome/storage';\nimport { ILocalStorageService, ISyncStorageService } from '@/service/common/storage';\n\nimport localeService from '@/common/locales';\nimport { IContentScriptService } from '@/service/common/contentScript';\nimport { IChannelServer } from '@/service/common/ipc';\nimport { ILocaleService } from '@/service/common/locale';\nimport '@/service/contentScript/browser/contentScript/contentScript';\nimport { ContentScriptChannel } from '@/service/contentScript/common/contentScriptIPC';\nimport '@/service/extension/browser/extensionContainer';\nimport { ContentScriptIPCServer } from '@/service/ipc/browser/contentScript/contentScriptIPCServer';\nimport { PopupIpcClient } from '@/service/ipc/browser/popup/ipcClient';\nimport { IWorkerService } from '@/service/worker/common';\nimport { WorkerServiceChannelClient } from '@/service/worker/common/workserServiceIPC';\nimport Container from 'typedi';\n\nContainer.set(ILocalStorageService, localStorageService);\nContainer.set(ISyncStorageService, syncStorageService);\nContainer.set(ILocaleService, localeService);\n\n//\nimport { IPreferenceService } from '@/service/common/preference';\nimport '@/service/preference/browser/preferenceService';\n\nlocaleService.init();\n\nconst backgroundIPCServer: IChannelServer = new ContentScriptIPCServer();\nbackgroundIPCServer.registerChannel(\n  'contentScript',\n  new ContentScriptChannel(Container.get(IContentScriptService))\n);\n\n(async () => {\n  await Container.get(ISyncStorageService).init();\n  updateColor();\n  Container.get(ISyncStorageService).onDidChangeStorage(() => {\n    updateColor();\n  });\n  updateMenu();\n})();\n\nconst ipcClient = new PopupIpcClient();\nconst workerChannel = ipcClient.getChannel('worker');\nContainer.set(IWorkerService, new WorkerServiceChannelClient(workerChannel));\n\nasync function updateColor() {\n  const preferenceService = Container.get(IPreferenceService);\n  await preferenceService.init();\n  Container.set(IWorkerService, new WorkerServiceChannelClient(workerChannel));\n  const workerService = Container.get(IWorkerService);\n  let iconColor = preferenceService.userPreference.iconColor;\n  if (iconColor === 'auto') {\n    const media = window.matchMedia('(prefers-color-scheme: dark)');\n    iconColor = media.matches ? 'light' : 'dark';\n  }\n  workerService.changeIcon(iconColor);\n}\n\nasync function updateMenu() {\n  const workerService = Container.get(IWorkerService);\n\n  workerService.initContextMenu();\n}\n"
  },
  {
    "path": "src/main/tool.main.chrome.ts",
    "content": "import 'regenerator-runtime/runtime';\nimport 'reflect-metadata';\nimport { ILocaleService } from '@/service/common/locale';\nimport Container from 'typedi';\nimport { IWebRequestService } from '@/service/common/webRequest';\nimport { WebRequestChannelClient } from '@/service/webRequest/common/webRequestIPC';\nimport { IContentScriptService } from '@/service/common/contentScript';\nimport { ContentScriptChannelClient } from '@/service/contentScript/common/contentScriptIPC';\nimport { ITabService } from '@/service/common/tab';\nimport { PopupIpcClient, PopupContentScriptIPCClient } from '@/service/ipc/browser/popup/ipcClient';\nimport '@/service/request/tool/basic';\nimport '@/service/config/browser/configService';\nimport localeService from '@/common/locales';\nContainer.set(ILocaleService, localeService);\nimport '@/service/extension/browser/extensionService';\nimport '@/service/extension/browser/extensionContainer';\nimport '@/service/permissions/chrome/permissionsService';\nimport { TabChannelClient } from '@/service/tab/common/tabIpc';\nimport app from '@/pages/app';\nimport { CookieChannelClient } from '@/service/cookie/common/cookieIpc';\nimport { ICookieService } from '@/service/common/cookie';\n\nconst ipcClient = new PopupIpcClient();\n\nconst tabChanel = ipcClient.getChannel('tab');\nContainer.set(ITabService, new TabChannelClient(tabChanel));\n\nconst contentScriptIPCClient = new PopupContentScriptIPCClient(Container.get(ITabService));\nconst contentScriptChannel = contentScriptIPCClient.getChannel('contentScript');\nContainer.set(IContentScriptService, new ContentScriptChannelClient(contentScriptChannel));\n\nconst webRequestChannel = ipcClient.getChannel('webRequest');\nContainer.set(IWebRequestService, new WebRequestChannelClient(webRequestChannel));\n\nconst cookieChannel = ipcClient.getChannel('cookies');\nContainer.set(ICookieService, new CookieChannelClient(cookieChannel));\n\napp();\n"
  },
  {
    "path": "src/models/account.ts",
    "content": "import update from 'immutability-helper';\nimport { DvaModelBuilder, removeActionNamespace } from 'dva-model-creator';\nimport { GlobalStore, AccountPreference } from '@/common/types';\nimport { syncStorageService } from '@/common/chrome/storage';\nimport {\n  initAccounts,\n  asyncAddAccount,\n  asyncDeleteAccount,\n  asyncUpdateDefaultAccountId,\n  asyncUpdateAccount,\n} from '@/actions/account';\nimport { asyncChangeAccount } from '@/actions/clipper';\nimport { message } from 'antd';\nimport { getServices } from '@/common/backend';\n\nconst initState: GlobalStore['account'] = {\n  accounts: [],\n};\n\nconst model = new DvaModelBuilder(initState, 'account');\n\nmodel\n  .subscript(async function loadAccounts({ dispatch }) {\n    syncStorageService.onDidChangeStorage(key => {\n      if (key === 'accounts') {\n        dispatch(removeActionNamespace(initAccounts.started()));\n      }\n      if (key === 'defaultAccountId') {\n        const defaultAccountId = syncStorageService.get('defaultAccountId');\n        dispatch(\n          removeActionNamespace(asyncUpdateDefaultAccountId.started({ id: defaultAccountId }))\n        );\n      }\n    });\n  })\n  .takeEvery(initAccounts.started, function*(_, { call, put }) {\n    let accountsString = yield call(syncStorageService.get, 'accounts', '[]');\n    const defaultAccountId: string = yield call(syncStorageService.get, 'defaultAccountId');\n    if (typeof accountsString !== 'string') {\n      accountsString = JSON.stringify(accountsString);\n    }\n    let accounts = <AccountPreference[]>JSON.parse(accountsString);\n    accounts = accounts.filter(account => getServices().some(o => o.type === account.type));\n    yield put(initAccounts.done({ result: { accounts, defaultAccountId } }));\n  })\n  .case(initAccounts.done, (s, { result: { accounts, defaultAccountId } }) => ({\n    ...s,\n    accounts,\n    defaultAccountId,\n  }));\n\nmodel.takeEvery(asyncAddAccount.started, function*(payload, { select, call }) {\n  const selector = ({ account: { accounts } }: GlobalStore) => {\n    return { accounts };\n  };\n  const { accounts }: ReturnType<typeof selector> = yield select(selector);\n  const { info, imageHosting, defaultRepositoryId, type, userInfo, callback, id } = payload;\n  const userPreference: AccountPreference = {\n    ...userInfo,\n    ...info,\n    imageHosting,\n    defaultRepositoryId,\n    type,\n    id,\n  };\n  if (accounts.some(o => o.id === userPreference.id)) {\n    message.error('Do not allow duplicate accounts');\n    return;\n  }\n  const newAccounts = update(accounts, {\n    $push: [userPreference],\n  });\n  if (newAccounts.length === 1) {\n    yield call(syncStorageService.set, 'defaultAccountId', userPreference.id);\n  }\n  callback();\n  yield call(syncStorageService.set, 'accounts', JSON.stringify(newAccounts));\n});\n\nmodel.takeEvery(asyncDeleteAccount.started, function*({ id }, { select, call }) {\n  const accounts: AccountPreference[] = yield select((g: GlobalStore) => g.account.accounts);\n  const defaultAccountId: string = yield select((g: GlobalStore) => g.account.defaultAccountId);\n  const newAccounts = accounts.filter(o => o.id !== id);\n  if (defaultAccountId === id) {\n    if (newAccounts.length > 0) {\n      yield call(syncStorageService.set, 'defaultAccountId', newAccounts[0].id);\n    } else {\n      yield call(syncStorageService.delete, 'defaultAccountId');\n    }\n  }\n  yield call(syncStorageService.set, 'accounts', JSON.stringify(newAccounts));\n});\n\nmodel\n  .takeEvery(asyncUpdateDefaultAccountId.started, function*({ id }, { call, put }) {\n    yield call(syncStorageService.set, 'defaultAccountId', id);\n    yield put(asyncUpdateDefaultAccountId.done({ params: { id } }));\n  })\n  .case(asyncUpdateDefaultAccountId.done, (s, { params: { id: defaultAccountId } }) => ({\n    ...s,\n    defaultAccountId,\n  }));\n\nmodel.takeEvery(asyncUpdateAccount, function*(payload, { select, put, call }) {\n  const selector = ({ account: { accounts, defaultAccountId } }: GlobalStore) => ({\n    accounts,\n    defaultAccountId,\n  });\n  const { accounts, defaultAccountId }: ReturnType<typeof selector> = yield select(selector);\n  const {\n    id,\n    account: { info, defaultRepositoryId, imageHosting },\n    userInfo,\n    newId,\n    callback,\n  } = payload;\n  const accountIndex = accounts.findIndex(o => o.id === id);\n  if (accountIndex < 0) {\n    message.error('Account Not Exist');\n    callback();\n    return;\n  }\n  const result = update(accounts, {\n    [accountIndex]: {\n      $merge: {\n        id: newId,\n        defaultRepositoryId,\n        imageHosting,\n        ...userInfo,\n        ...info,\n      },\n    },\n  });\n  yield call(syncStorageService.set, 'accounts', JSON.stringify(result));\n  const currentAccountId: string = yield select((g: GlobalStore) => g.clipper.currentAccountId);\n  if (id === defaultAccountId) {\n    yield put.resolve(asyncUpdateDefaultAccountId.started({ id: newId }));\n  }\n  yield put.resolve(initAccounts.started);\n  callback();\n  if (id === currentAccountId) {\n    yield put.resolve(asyncChangeAccount.started({ id: newId }));\n  }\n});\n\nexport default model.build();\n"
  },
  {
    "path": "src/models/clipper.tsx",
    "content": "import { Container } from 'typedi';\nimport React from 'react';\nimport { IPermissionsService } from '@/service/common/permissions';\nimport { BUILT_IN_IMAGE_HOSTING_ID } from '@/common/backend/imageHosting/interface';\nimport { updateClipperHeader } from './../actions/clipper';\nimport { asyncRunExtension } from './../actions/userPreference';\nimport { CompleteStatus } from 'common/backend/interface';\nimport { CreateDocumentRequest, UnauthorizedError } from '@/common/backend/services/interface';\nimport { GlobalStore, ClipperStore } from '@/common/types';\nimport { DvaModelBuilder, removeActionNamespace } from 'dva-model-creator';\nimport update from 'immutability-helper';\nimport {\n  selectRepository,\n  initTabInfo,\n  asyncCreateDocument,\n  asyncChangeAccount,\n  changeData,\n  watchActionChannel,\n} from 'pageActions/clipper';\nimport backend, { documentServiceFactory, imageHostingServiceFactory } from 'common/backend';\nimport { unpackAccountPreference } from '@/services/account/common';\nimport { notification, Button } from 'antd';\nimport { routerRedux } from 'dva';\nimport { asyncUpdateAccount } from '@/actions/account';\nimport { channel } from 'redux-saga';\nimport { IExtensionService, IExtensionContainer } from '@/service/common/extension';\nimport { ExtensionType } from '@/extensions/common';\n\nconst defaultState: ClipperStore = {\n  clipperHeaderForm: {\n    title: '',\n  },\n  currentAccountId: '',\n  repositories: [],\n  clipperData: {},\n};\n\nconst actionChannel = channel();\n\nconst model = new DvaModelBuilder(defaultState, 'clipper')\n  .subscript(function startWatchActionChannel({ dispatch }) {\n    dispatch(removeActionNamespace(watchActionChannel()));\n  })\n  .takeEvery(watchActionChannel, function*(_, { put, take }) {\n    while (true) {\n      //@ts-ignore\n      const action = yield take(actionChannel);\n      yield put(action);\n    }\n  })\n  .takeEvery(asyncChangeAccount.started, function*(payload, { call, select, put }) {\n    const selector = ({\n      userPreference: { imageHosting, servicesMeta },\n      account: { accounts },\n    }: GlobalStore) => {\n      return {\n        accounts,\n        imageHosting,\n        servicesMeta,\n      };\n    };\n    const selectState: ReturnType<typeof selector> = yield select(selector);\n    const { accounts, imageHosting } = selectState;\n    let currentAccount = accounts.find(o => o.id === payload.id);\n    if (!currentAccount) {\n      return;\n    }\n    const {\n      id,\n      account,\n      account: { type, info },\n      userInfo,\n    } = unpackAccountPreference(currentAccount);\n    const documentService = documentServiceFactory(type, info);\n    const permissionsService = Container.get(IPermissionsService);\n    if (selectState.servicesMeta[type]?.permission) {\n      //@ts-ignore\n      const hasPermissions = yield call(\n        permissionsService.contains,\n        selectState.servicesMeta[type]?.permission!\n      );\n      if (!hasPermissions) {\n        const key = `open${Date.now()}`;\n        const close = () => {\n          permissionsService.request(selectState.servicesMeta[type]?.permission!).then(re => {\n            if (re) {\n              actionChannel.put(asyncChangeAccount.started({ id }));\n            }\n          });\n        };\n        notification.error({\n          key,\n          placement: 'topRight',\n          duration: 0,\n          message: 'No Permission',\n          btn: (\n            <Button\n              onClick={() => {\n                notification.close(key);\n                close();\n              }}\n              type=\"primary\"\n            >\n              Grant\n            </Button>\n          ),\n          onClose: () => close,\n        });\n        return;\n      }\n    }\n    let repositories = [];\n    try {\n      repositories = yield call(documentService.getRepositories);\n    } catch (error) {\n      if (error instanceof UnauthorizedError) {\n        if (documentService.refreshToken) {\n          const newInfo = yield call(documentService.refreshToken, info);\n          yield put(\n            asyncUpdateAccount({\n              id,\n              account: {\n                ...account,\n                info: newInfo,\n              },\n              userInfo,\n              newId: id,\n              callback: () => {\n                actionChannel.put(asyncChangeAccount.started({ id }));\n              },\n            })\n          );\n          return;\n        }\n        throw new Error('Filed to load Repositories,Unauthorized.');\n      } else {\n        throw error;\n      }\n    }\n    backend.setDocumentService(documentService);\n    let currentImageHostingService: ClipperStore['currentImageHostingService'];\n    if (account.imageHosting) {\n      if (account.imageHosting === BUILT_IN_IMAGE_HOSTING_ID) {\n        currentImageHostingService = {\n          type: type,\n        };\n        const imageHostingService = imageHostingServiceFactory(type, info);\n        backend.setImageHostingService(imageHostingService);\n      } else {\n        const imageHostingIndex = imageHosting.findIndex(o => o.id === account.imageHosting);\n        if (imageHostingIndex !== -1) {\n          const accountImageHosting = imageHosting[imageHostingIndex];\n          const imageHostingService = imageHostingServiceFactory(\n            accountImageHosting.type,\n            accountImageHosting.info\n          );\n          backend.setImageHostingService(imageHostingService);\n          currentImageHostingService = {\n            type: accountImageHosting.type,\n          };\n        }\n      }\n    }\n    yield put(\n      asyncChangeAccount.done({\n        params: payload,\n        result: {\n          repositories,\n          currentImageHostingService,\n        },\n      })\n    );\n  })\n  .takeLatest(asyncCreateDocument.started, function*({ pathname }, { put, call, select }) {\n    const selector = ({\n      clipper: { currentRepository, clipperHeaderForm, repositories, currentAccountId },\n      account: { accounts },\n    }: GlobalStore) => {\n      const currentAccount = accounts.find(({ id }) => id === currentAccountId);\n      let repositoryId;\n      if (\n        currentAccount &&\n        repositories.some(({ id }) => id === currentAccount.defaultRepositoryId)\n      ) {\n        repositoryId = currentAccount.defaultRepositoryId;\n      }\n      if (currentRepository) {\n        repositoryId = currentRepository.id;\n      }\n      const extensions = Container.get(IExtensionContainer).extensions;\n      const extension = extensions.find(o => o.router === pathname);\n      const enabledAutomaticExtensionIds = Container.get(IExtensionService)\n        .EnabledAutomaticExtensionIds;\n      const automaticExtensions = extensions.filter(\n        o =>\n          o.type === ExtensionType.Tool &&\n          o.manifest.automatic &&\n          enabledAutomaticExtensionIds.some(id => id === o.id)\n      );\n      return {\n        repositoryId,\n        extensions,\n        clipperHeaderForm,\n        extension,\n        repositories,\n        automaticExtensions,\n      };\n    };\n    const {\n      repositoryId,\n      clipperHeaderForm,\n      extension,\n      automaticExtensions,\n    }: ReturnType<typeof selector> = yield select(selector);\n    if (!repositoryId) {\n      yield put(\n        asyncCreateDocument.failed({\n          params: { pathname },\n          error: null,\n        })\n      );\n      throw new Error('Must select repository.');\n    }\n    if (!extension) {\n      // DEBT\n      if (pathname !== '/editor') {\n        return;\n      }\n    }\n    for (const iterator of automaticExtensions) {\n      // DEBT\n      if (iterator.id === 'web-clipper/link.' && pathname === '/editor') {\n        continue;\n      }\n      yield put.resolve(asyncRunExtension.started({ pathname, extension: iterator }));\n    }\n    const { data, url } = yield select((g: GlobalStore) => {\n      return {\n        url: g.clipper.url,\n        data: g.clipper.clipperData[pathname],\n      };\n    });\n    let createDocumentRequest: CreateDocumentRequest | null = null;\n    createDocumentRequest = {\n      repositoryId,\n      content: data as string,\n      url,\n      ...clipperHeaderForm,\n    };\n    if (!createDocumentRequest) {\n      return;\n    }\n    const response: CompleteStatus = yield call(\n      backend.getDocumentService()!.createDocument,\n      createDocumentRequest\n    );\n    yield put(\n      asyncCreateDocument.done({\n        params: { pathname },\n        result: {\n          result: response,\n          request: createDocumentRequest,\n        },\n      })\n    );\n    yield put(routerRedux.push('/complete'));\n  })\n  .case(\n    asyncChangeAccount.done,\n    (state, { params: { id }, result: { repositories, currentImageHostingService } }) => {\n      return update(state, {\n        currentAccountId: {\n          $set: id,\n        },\n        repositories: {\n          $set: repositories,\n        },\n        currentRepository: {\n          // eslint-disable-next-line no-undefined\n          $set: undefined,\n        },\n        currentImageHostingService: {\n          $set: currentImageHostingService,\n        },\n      });\n    }\n  )\n  .case(selectRepository, (state, { repositoryId }) => {\n    const currentRepository = state.repositories.find(o => o.id === repositoryId);\n    const updateContext = backend.getImageHostingService()?.updateContext;\n    if (currentRepository && updateContext) {\n      updateContext({ currentRepository });\n    }\n    return {\n      ...state,\n      currentRepository,\n    };\n  })\n  .case(initTabInfo, (state, { title, url }) => ({\n    ...state,\n    clipperHeaderForm: {\n      ...state.clipperHeaderForm,\n      title,\n    },\n    url,\n  }))\n  .case(asyncCreateDocument.started, state => ({\n    ...state,\n  }))\n  .case(\n    asyncCreateDocument.done,\n    (state, { result: { result: completeStatus, request: createDocumentRequest } }) => ({\n      ...state,\n      completeStatus,\n      createDocumentRequest,\n    })\n  )\n  .case(updateClipperHeader, (state, clipperHeaderForm) => ({\n    ...state,\n    clipperHeaderForm,\n  }))\n  .case(changeData, (state, { data, pathName }) => {\n    return update(state, {\n      clipperData: {\n        [pathName]: {\n          $set: data,\n        },\n      },\n    });\n  });\n\nexport default model.build();\n"
  },
  {
    "path": "src/models/userPreference.ts",
    "content": "import { IContentScriptService } from '@/service/common/contentScript';\nimport { ITabService } from '@/service/common/tab';\nimport { Container } from 'typedi';\nimport React from 'react';\nimport { getLanguage } from './../common/locales';\nimport localeService from '@/common/locales';\nimport { LOCAL_USER_PREFERENCE_LOCALE_KEY } from './../common/modelTypes/userPreference';\nimport storage from 'common/storage';\nimport * as antd from 'antd';\nimport { GlobalStore } from '@/common/types';\nimport update from 'immutability-helper';\nimport {\n  asyncSetEditorLiveRendering,\n  initUserPreference,\n  asyncDeleteImageHosting,\n  asyncAddImageHosting,\n  asyncEditImageHosting,\n  asyncRunExtension,\n  setLocale,\n  asyncSetLocaleToStorage,\n  initServices,\n  asyncSetIconColor,\n} from 'pageActions/userPreference';\nimport { initTabInfo, changeData, asyncChangeAccount } from 'pageActions/clipper';\nimport { DvaModelBuilder, removeActionNamespace } from 'dva-model-creator';\nimport { UserPreferenceStore } from 'common/types';\nimport { getServices, getImageHostingServices, imageHostingServiceFactory } from 'common/backend';\nimport { ToolContext } from '@/extensions/common';\nimport backend from 'common/backend/index';\nimport { loadImage } from 'common/blob';\nimport { routerRedux } from 'dva';\nimport { localStorageService, syncStorageService } from '@/common/chrome/storage';\nimport { initAccounts } from '@/actions/account';\nimport copyToClipboard from 'copy-to-clipboard';\nimport remark from 'remark';\nimport remakPangu from '@web-clipper/remark-pangu';\nimport { IExtensionService } from '@/service/common/extension';\n\nconst { message } = antd;\n\nconst defaultState: UserPreferenceStore = {\n  locale: getLanguage(),\n  imageHosting: [],\n  servicesMeta: {},\n  imageHostingServicesMeta: {},\n  liveRendering: true,\n  iconColor: 'auto',\n};\n\nconst builder = new DvaModelBuilder(defaultState, 'userPreference')\n  .case(asyncSetIconColor.done, (state, { result: { value: iconColor } }) => ({\n    ...state,\n    iconColor,\n  }))\n  .case(asyncSetEditorLiveRendering.done, (state, { result: { value: liveRendering } }) => ({\n    ...state,\n    liveRendering,\n  }))\n  .case(initUserPreference, (state, payload) => ({\n    ...state,\n    ...payload,\n  }))\n  .case(asyncDeleteImageHosting.done, (state, { result }) =>\n    update(state, {\n      imageHosting: {\n        $set: result,\n      },\n    })\n  )\n  .case(asyncAddImageHosting.done, (state, { result }) =>\n    update(state, {\n      imageHosting: {\n        $set: result,\n      },\n    })\n  )\n  .case(asyncEditImageHosting.done, (state, { result }) =>\n    update(state, {\n      imageHosting: {\n        $set: result,\n      },\n    })\n  );\n\nbuilder\n  .takeEvery(asyncSetIconColor.started, function*({ value }, { call, put }) {\n    yield call(storage.setIconColor, value);\n    yield put(\n      asyncSetIconColor.done({\n        params: {\n          value,\n        },\n        result: {\n          value: value,\n        },\n      })\n    );\n  })\n  .takeEvery(asyncSetEditorLiveRendering.started, function*({ value }, { call, put }) {\n    yield call(storage.setLiveRendering, !value);\n    yield put(\n      asyncSetEditorLiveRendering.done({\n        params: {\n          value,\n        },\n        result: {\n          value: !value,\n        },\n      })\n    );\n  })\n  .takeEvery(asyncEditImageHosting.started, function*(payload, { call, put }) {\n    const { id, value, closeModal } = payload;\n    try {\n      //@ts-ignore\n      const imageHostingList = yield call(storage.editImageHostingById, id, {\n        ...value,\n        id,\n      });\n      yield put(\n        asyncEditImageHosting.done({\n          params: payload,\n          result: imageHostingList,\n        })\n      );\n      closeModal();\n    } catch (error) {\n      message.error((error as Error).message);\n    }\n  })\n  .takeEvery(asyncDeleteImageHosting.started, function*(payload, { call, put }) {\n    const imageHostingList: PromiseType<ReturnType<\n      typeof storage.deleteImageHostingById\n    >> = yield call(storage.deleteImageHostingById, payload.id);\n    yield put(\n      asyncDeleteImageHosting.done({\n        params: payload,\n        result: imageHostingList,\n      })\n    );\n  })\n  .takeEvery(asyncAddImageHosting.started, function*(payload, { call, put }) {\n    const { info, type, closeModal, remark } = payload;\n    const imageHostingService: ReturnType<typeof imageHostingServiceFactory> = yield call(\n      imageHostingServiceFactory,\n      type,\n      info\n    );\n    if (!imageHostingService) {\n      message.error('不支持');\n      return;\n    }\n    const id = imageHostingService.getId();\n    const imageHosting = {\n      id,\n      type,\n      info,\n      remark,\n    };\n    try {\n      const imageHostingList: PromiseType<ReturnType<typeof storage.addImageHosting>> = yield call(\n        storage.addImageHosting,\n        imageHosting\n      );\n      yield put(\n        asyncAddImageHosting.done({\n          params: payload,\n          result: imageHostingList,\n        })\n      );\n      closeModal();\n    } catch (error) {\n      message.error((error as Error).message);\n    }\n  })\n  .takeEvery(asyncRunExtension.started, function*({ extension, pathname }, { call, put, select }) {\n    const contentScriptService = Container.get(IContentScriptService);\n    let result;\n    const {\n      extensionLifeCycle: { run, afterRun, destroy },\n      id,\n      manifest,\n    } = extension;\n    const tabService = Container.get(ITabService);\n    const extensionService = Container.get(IExtensionService);\n    let config: any;\n    if (manifest.extensionId) {\n      config =\n        extensionService.getExtensionConfig(manifest.extensionId) || manifest.config?.default;\n      if (Array.isArray(config?.autoRunExclude) && config?.autoRunExclude.length > 0) {\n        const autoRunExclude: string[] = config?.autoRunExclude;\n        // TODO\n        if (autoRunExclude.some(p => `/plugins/${p}` === pathname)) {\n          return;\n        }\n      }\n    }\n\n    if (run) {\n      //@ts-ignore\n      result = yield call(contentScriptService.runScript, id, 'run');\n    }\n    const state: GlobalStore = yield select(state => state);\n    const data = state.clipper.clipperData[pathname];\n\n    function createAndDownloadFile(fileName: string, content: string | Blob) {\n      let aTag = document.createElement('a');\n      let blob: Blob;\n      if (typeof content === 'string') {\n        blob = new Blob([content]);\n      } else {\n        blob = content;\n      }\n      aTag.download = fileName;\n      aTag.href = URL.createObjectURL(blob);\n      aTag.click();\n      URL.revokeObjectURL(aTag.href);\n    }\n    async function pangu(document: string): Promise<string> {\n      const result = await remark()\n        .use(remakPangu)\n        .process(document);\n      return result.contents as string;\n    }\n    if (afterRun) {\n      try {\n        const context: ToolContext<any, any> = {\n          locale: state.userPreference.locale,\n          result,\n          data,\n          message,\n          imageService: backend.getImageHostingService(),\n          loadImage: loadImage,\n          captureVisibleTab: tabService.captureVisibleTab,\n          copyToClipboard,\n          createAndDownloadFile,\n          antd,\n          React,\n          pangu,\n          config,\n        };\n        //@ts-ignore\n        result = yield call(afterRun, context);\n      } catch (error) {\n        message.error((error as Error).message);\n      }\n    }\n    if (destroy) {\n      contentScriptService.runScript(id, 'destroy');\n    }\n    yield put(\n      changeData({\n        data: result,\n        pathName: pathname,\n      })\n    );\n  });\n\nbuilder.subscript(async function initStore({ dispatch, history }) {\n  await dispatch(initAccounts.started());\n  const result = await storage.getPreference();\n  const tabService = Container.get(ITabService);\n  const tabInfo = await tabService.getCurrent();\n  if (tabInfo.title && tabInfo.url) {\n    dispatch(initTabInfo({ title: tabInfo.title, url: tabInfo.url }));\n  }\n  dispatch(removeActionNamespace(initUserPreference(result)));\n  if (history.location.pathname !== '/' && history.location.pathname !== '/editor') {\n    return;\n  }\n  if (result.defaultPluginId) {\n    dispatch(routerRedux.push(`/plugins/${result.defaultPluginId}`));\n  }\n  const defaultAccountId = syncStorageService.get('defaultAccountId');\n  if (defaultAccountId) {\n    dispatch(asyncChangeAccount.started({ id: defaultAccountId }));\n  }\n});\n\nbuilder\n  .takeEvery(asyncSetLocaleToStorage, function*(locale, { call }) {\n    yield call(localStorageService.set, LOCAL_USER_PREFERENCE_LOCALE_KEY, locale);\n  })\n  .subscript(async function initLocal({ dispatch }) {\n    const locale = localStorageService.get(LOCAL_USER_PREFERENCE_LOCALE_KEY, navigator.language);\n    dispatch(removeActionNamespace(setLocale(locale)));\n    localStorageService.onDidChangeStorage(key => {\n      if (key === LOCAL_USER_PREFERENCE_LOCALE_KEY) {\n        dispatch(\n          removeActionNamespace(\n            setLocale(localStorageService.get(LOCAL_USER_PREFERENCE_LOCALE_KEY, navigator.language))\n          )\n        );\n      }\n    });\n  })\n  .case(setLocale, (state, locale) => ({ ...state, locale }));\n\nbuilder\n  .subscript(async function xx({ dispatch }) {\n    const servicesMeta = getServices().reduce((previousValue, meta) => {\n      previousValue[meta.type] = meta;\n      return previousValue;\n    }, {} as UserPreferenceStore['servicesMeta']);\n\n    const imageHostingServicesMeta = getImageHostingServices().reduce((previousValue, meta) => {\n      previousValue[meta.type] = meta;\n      return previousValue;\n    }, {} as UserPreferenceStore['imageHostingServicesMeta']);\n    dispatch(\n      removeActionNamespace(\n        initServices({\n          imageHostingServicesMeta,\n          servicesMeta,\n        })\n      )\n    );\n\n    localStorageService.onDidChangeStorage(async key => {\n      if (key === LOCAL_USER_PREFERENCE_LOCALE_KEY) {\n        await localeService.init();\n        const servicesMeta = getServices().reduce((previousValue, meta) => {\n          previousValue[meta.type] = meta;\n          return previousValue;\n        }, {} as UserPreferenceStore['servicesMeta']);\n        const imageHostingServicesMeta = getImageHostingServices().reduce((previousValue, meta) => {\n          previousValue[meta.type] = meta;\n          return previousValue;\n        }, {} as UserPreferenceStore['imageHostingServicesMeta']);\n        dispatch(\n          removeActionNamespace(\n            initServices({\n              imageHostingServicesMeta,\n              servicesMeta,\n            })\n          )\n        );\n      }\n    });\n  })\n  .case(initServices, (state, { imageHostingServicesMeta, servicesMeta }) => {\n    return {\n      ...state,\n      imageHostingServicesMeta,\n      servicesMeta,\n    };\n  });\n\nexport default builder.build();\n"
  },
  {
    "path": "src/pages/app.less",
    "content": "body {\n  background: none;\n  font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, PingFang SC,\n    Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n}\n"
  },
  {
    "path": "src/pages/app.tsx",
    "content": "import 'antd/dist/antd.less';\nimport Container from 'typedi';\nimport React from 'react';\nimport dva, { router } from 'dva';\nimport { createHashHistory } from 'history';\nimport preference from '@/pages/preference';\nimport Complete from '@/pages/complete/complete';\nimport PluginPage from '@/pages/plugin/Page';\nimport Tool from '@/pages/tool';\nimport clipper from '@/models/clipper';\nimport userPreference from '@/models/userPreference';\nimport createLoading from 'dva-loading';\nimport LocalWrapper from './locale';\nimport { localStorageService, syncStorageService } from '@/common/chrome/storage';\nimport localeService from '@/common/locales';\nimport AuthPage from '@/pages/auth';\nimport account from '@/models/account';\nimport { message } from 'antd';\nimport { IConfigService } from '@/service/common/config';\nimport { ILocalStorageService, ISyncStorageService } from '@/service/common/storage';\nimport './app.less';\nContainer.set(ILocalStorageService, localStorageService);\nContainer.set(ISyncStorageService, syncStorageService);\nimport '@/service/preference/browser/preferenceService';\nimport { IPreferenceService } from '@/service/common/preference';\nimport '@/services/environment/common/environmentService';\nconst { Route, Switch, Router, withRouter } = router;\n\nfunction withTool(WrappedComponent: any): any {\n  return () => {\n    const ToolWith = withRouter(Tool as any);\n    const WrappedComponentWith = withRouter(WrappedComponent);\n\n    return (\n      <React.Fragment>\n        <ToolWith></ToolWith>\n        <WrappedComponentWith></WrappedComponentWith>\n      </React.Fragment>\n    );\n  };\n}\n\nexport default async () => {\n  await syncStorageService.init();\n  await localStorageService.init();\n  await localeService.init();\n  Container.get(IConfigService).load();\n  await Container.get(IPreferenceService).init();\n  const app = dva({\n    namespacePrefixWarning: false,\n    history: createHashHistory(),\n    onError: (e) => {\n      (e as any).preventDefault();\n      message.destroy();\n      message.error(e.message);\n      message.error(e.stack);\n    },\n  });\n  app.use(createLoading());\n\n  app.router((router) => {\n    return (\n      <LocalWrapper>\n        <Router history={router!.history}>\n          <Switch>\n            <Route exact path=\"/\" component={Tool} />\n            <Route exact path=\"/auth\" component={AuthPage} />\n            <Route exact path=\"/complete\" component={Complete} />\n            <Route path=\"/editor\" component={withTool(PluginPage)} />\n            <Route path=\"/preference/:id\" component={withTool(preference)} />\n            <Route path=\"/plugins/:id\" component={withTool(PluginPage)} />\n          </Switch>\n        </Router>\n      </LocalWrapper>\n    );\n  });\n\n  app.model(account);\n  app.model(clipper);\n  app.model(userPreference);\n  app.start('#app');\n};\n"
  },
  {
    "path": "src/pages/auth.tsx",
    "content": "import React, { useEffect, useMemo } from 'react';\nimport { connect } from 'dva';\nimport { parse } from 'qs';\nimport { DvaRouterProps, GlobalStore } from '@/common/types';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Modal, Select } from 'antd';\nimport { FormattedMessage } from 'react-intl';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport useVerifiedAccount from '@/common/hooks/useVerifiedAccount';\nimport ImageHostingSelect from '@/components/ImageHostingSelect';\nimport useFilterImageHostingServices, {\n  ImageHostingWithMeta,\n} from '@/common/hooks/useFilterImageHostingServices';\nimport { asyncAddAccount } from '@/actions/account';\nimport { isEqual } from 'lodash';\nimport RepositorySelect from '@/components/RepositorySelect';\nimport { BUILT_IN_IMAGE_HOSTING_ID } from '@/common/backend/imageHosting/interface';\nimport Container from 'typedi';\nimport { ITabService } from '@/service/common/tab';\n\ninterface PageQuery {\n  access_token: string;\n  type: string;\n}\n\nconst mapStateToProps = ({\n  userPreference: { servicesMeta, imageHosting, imageHostingServicesMeta },\n}: GlobalStore) => {\n  return {\n    servicesMeta,\n    imageHosting,\n    imageHostingServicesMeta,\n  };\n};\ntype PageStateProps = ReturnType<typeof mapStateToProps>;\ntype PageProps = PageStateProps & DvaRouterProps & FormComponentProps;\n\nfunction useDeepCompareMemoize<T>(value: T) {\n  const ref = React.useRef<T>();\n  if (!isEqual(value, ref.current)) {\n    ref.current = value;\n  }\n  return ref.current;\n}\n\nconst Page: React.FC<PageProps> = props => {\n  const query: PageQuery = parse(props.location.search.slice(1)) as any;\n  const tabService = Container.get(ITabService);\n  const {\n    form: { getFieldDecorator },\n    form,\n    imageHosting,\n    imageHostingServicesMeta,\n  } = props;\n\n  const {\n    type,\n    verifyAccount,\n    accountStatus: { repositories, verified, userInfo, id },\n    serviceForm,\n    verifying,\n    okText,\n  } = useVerifiedAccount({\n    form: props.form,\n    services: props.servicesMeta,\n    initAccount: query,\n  });\n\n  const imageHostingWithBuiltIn = useMemo(() => {\n    const res = [...imageHosting];\n    const meta = imageHostingServicesMeta[type];\n    if (meta?.builtIn) {\n      res.push({\n        type,\n        info: {},\n        id: BUILT_IN_IMAGE_HOSTING_ID,\n        remark: meta.builtInRemark,\n      });\n    }\n    return res;\n  }, [imageHosting, imageHostingServicesMeta, type]);\n\n  const supportedImageHostingServices: ImageHostingWithMeta[] = useFilterImageHostingServices({\n    backendServiceType: type,\n    imageHostingServices: imageHostingWithBuiltIn,\n    imageHostingServicesMap: imageHostingServicesMeta,\n  });\n\n  const memoizeQuery = useDeepCompareMemoize(query);\n\n  useEffect(() => {\n    verifyAccount(memoizeQuery);\n  }, [verifyAccount, memoizeQuery]);\n\n  return (\n    <Modal\n      visible\n      okText={okText}\n      onCancel={tabService.closeCurrent}\n      okButtonProps={{\n        disabled: verifying,\n        loading: verifying,\n      }}\n      title={<FormattedMessage id=\"auth.modal.title\" />}\n      onOk={() => {\n        form.validateFields((error, values) => {\n          if (error) {\n            return;\n          }\n          const { defaultRepositoryId, imageHosting, ...info } = values;\n          props.dispatch(\n            asyncAddAccount.started({\n              id: id!,\n              type,\n              defaultRepositoryId,\n              imageHosting,\n              info,\n              userInfo: userInfo!,\n              callback: tabService.closeCurrent,\n            })\n          );\n        });\n      }}\n    >\n      <Form labelCol={{ span: 7, offset: 0 }} wrapperCol={{ span: 17 }}>\n        <Form.Item\n          label={<FormattedMessage id=\"preference.accountList.type\" defaultMessage=\"Type\" />}\n        >\n          {getFieldDecorator('type', {\n            initialValue: query.type,\n          })(\n            <Select disabled>\n              {Object.values(props.servicesMeta).map(o => (\n                <Select.Option key={o.type} value={o.type}>\n                  {o.name}\n                </Select.Option>\n              ))}\n            </Select>\n          )}\n        </Form.Item>\n        {serviceForm}\n        <Form.Item\n          label={\n            <FormattedMessage\n              id=\"preference.accountList.defaultRepository\"\n              defaultMessage=\"Default Repository\"\n            />\n          }\n        >\n          {getFieldDecorator('defaultRepositoryId')(\n            <RepositorySelect\n              disabled={!verified}\n              loading={verifying}\n              repositories={repositories}\n            />\n          )}\n        </Form.Item>\n        <Form.Item\n          label={\n            <FormattedMessage id=\"preference.accountList.imageHost\" defaultMessage=\"Image Host\" />\n          }\n        >\n          {getFieldDecorator('imageHosting')(\n            <ImageHostingSelect\n              disabled={!verified}\n              supportedImageHostingServices={supportedImageHostingServices}\n            />\n          )}\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default connect(mapStateToProps)(Form.create<PageProps>()(Page));\n"
  },
  {
    "path": "src/pages/complete/complete.less",
    "content": ".jump {\n  margin-top: 16px;\n}\n.icons {\n  font-size: 100px;\n}\n"
  },
  {
    "path": "src/pages/complete/complete.tsx",
    "content": "import * as React from 'react';\nimport { useSelector } from 'dva';\nimport { ToolContainer } from 'components/container';\nimport styles from './complete.less';\nimport { Button } from 'antd';\nimport { GlobalStore } from '@/common/types';\nimport Section from 'components/section';\nimport { FormattedMessage } from 'react-intl';\nimport Share from '@/components/share';\nimport Container from 'typedi';\nimport { IContentScriptService } from '@/service/common/contentScript';\n\nconst Page: React.FC = () => {\n  const { servicesMeta, currentAccount, completeStatus, createDocumentRequest } = useSelector(\n    ({\n      clipper: { completeStatus, currentAccountId, createDocumentRequest },\n      userPreference: { servicesMeta },\n      account: { accounts },\n    }: GlobalStore) => {\n      const currentAccount = accounts.find(o => o.id === currentAccountId);\n      return {\n        servicesMeta,\n        currentAccount,\n        completeStatus,\n        createDocumentRequest,\n      };\n    }\n  );\n  function closeTool() {\n    Container.get(IContentScriptService).remove();\n  }\n  const renderError = (\n    <ToolContainer onClickCloseButton={closeTool}>\n      <a target=\"_blank\" href=\"https://github.com/webclipper/web-clipper/issues\">\n        <FormattedMessage id=\"page.complete.error\" defaultMessage=\"Some Error\" />\n      </a>\n    </ToolContainer>\n  );\n  if (!currentAccount) {\n    return renderError;\n  }\n  const currentService = servicesMeta[currentAccount.type];\n  if (!currentService) {\n    return renderError;\n  }\n  const { name, complete: Complete } = currentService;\n  return (\n    <ToolContainer onClickCloseButton={closeTool} onClickMask={closeTool}>\n      <Section title={<FormattedMessage id=\"page.complete.success\" defaultMessage=\"Success\" />}>\n        {completeStatus?.href ? (\n          <a href={completeStatus.href} target=\"_blank\">\n            <Button className={styles.jump} size=\"large\" type=\"primary\" block>\n              <FormattedMessage\n                id=\"page.complete.message\"\n                defaultMessage=\"Go to {name}\"\n                values={{ name: <span>{name}</span> }}\n              />\n            </Button>\n          </a>\n        ) : (\n          <Button className={styles.jump} size=\"large\" type=\"primary\" block onClick={closeTool}>\n            <FormattedMessage id=\"page.complete.close\" defaultMessage=\"Close Web Clipper\" />\n          </Button>\n        )}\n      </Section>\n      {Complete && <Complete status={completeStatus}> </Complete>}\n      <Section title={<FormattedMessage id=\"page.complete.share\" defaultMessage=\"Share\" />}>\n        <Share content={createDocumentRequest!.content}></Share>\n      </Section>\n    </ToolContainer>\n  );\n};\n\nexport default Page;\n"
  },
  {
    "path": "src/pages/locale.tsx",
    "content": "import React from 'react';\nimport { IntlProvider } from 'react-intl';\nimport { ConfigProvider } from 'antd';\nimport { connect } from 'dva';\nimport { localesMap } from '@/common/locales';\nimport { localeProvider } from '@/common/locales/antd';\nimport { GlobalStore } from '@/common/types';\n\nconst mapStateToProps = ({ userPreference: { locale } }: GlobalStore) => {\n  return {\n    locale,\n  };\n};\ntype PageStateProps = ReturnType<typeof mapStateToProps>;\n\nconst LocalWrapper: React.FC<PageStateProps> = ({ children, locale }) => {\n  const language = locale;\n  const model = (localesMap.get(language) || localesMap.get('en-US'))!;\n  return (\n    <IntlProvider key={locale} locale={language} messages={model.messages}>\n      <ConfigProvider\n        locale={localeProvider[model.locale as keyof typeof localeProvider]}\n        getPopupContainer={e => {\n          if (!e || !e.parentNode) {\n            return document.body;\n          }\n          return e.parentNode as HTMLElement;\n        }}\n      >\n        {children}\n      </ConfigProvider>\n    </IntlProvider>\n  );\n};\n\nexport default connect(mapStateToProps)(LocalWrapper);\n"
  },
  {
    "path": "src/pages/plugin/Page.tsx",
    "content": "import React from 'react';\nimport { connect, router } from 'dva';\nimport { ExtensionType } from '@/extensions/common';\nimport TextEditor from './TextEditor';\nimport { DvaRouterProps } from '@/common/types';\nimport { useObserver } from 'mobx-react';\nimport Container from 'typedi';\nimport { IExtensionContainer } from '@/service/common/extension';\n\nconst { Redirect } = router;\n\nconst ClipperPluginPage: React.FC<DvaRouterProps> = props => {\n  const {\n    history: {\n      location: { pathname, search },\n    },\n  } = props;\n  const extensions = useObserver(() => Container.get(IExtensionContainer).extensions);\n  if (pathname === '/editor') {\n    return <TextEditor extension={null} pathname={pathname} search={search} />;\n  }\n  const extension = extensions.find(o => o.router === pathname);\n  if (!extension) {\n    return <Redirect to=\"/\"></Redirect>;\n  }\n  if (extension.type === ExtensionType.Text) {\n    return <TextEditor extension={extension} pathname={pathname} search={search} />;\n  }\n  return <Redirect to=\"/\"></Redirect>;\n};\n\nexport default connect()(ClipperPluginPage);\n"
  },
  {
    "path": "src/pages/plugin/TextEditor.tsx",
    "content": "import React from 'react';\nimport { bindActionCreators, Dispatch } from 'redux';\nimport { connect } from 'dva';\nimport { changeData } from 'pageActions/clipper';\nimport { asyncRunExtension } from 'pageActions/userPreference';\nimport * as HyperMD from 'hypermd';\nimport { EditorContainer } from 'components/container';\nimport { isUndefined } from 'common/object';\nimport { GlobalStore } from 'common/types';\nimport { IExtensionWithId } from '@/extensions/common';\nimport { parse } from 'qs';\n\nconst useActions = {\n  asyncRunExtension: asyncRunExtension.started,\n  changeData,\n};\n\nconst mapStateToProps = ({\n  clipper: { clipperData },\n  userPreference: { liveRendering },\n}: GlobalStore) => {\n  return {\n    liveRendering,\n    clipperData,\n  };\n};\ntype PageOwnProps = {\n  pathname: string;\n  search?: string;\n  extension: IExtensionWithId | null;\n};\ntype PageProps = ReturnType<typeof mapStateToProps> & typeof useActions & PageOwnProps;\n\nconst editorId = 'DiamondYuan_Love_LJ';\n\nclass ClipperPluginPage extends React.Component<PageProps, { markdown: string }> {\n  private myCodeMirror: any;\n\n  constructor(props: any) {\n    super(props);\n    this.state = {\n      markdown: '',\n    };\n  }\n\n  checkExtension = () => {\n    const { extension, clipperData, pathname, search } = this.props;\n    const data = clipperData[pathname];\n    if (isUndefined(data) && extension) {\n      this.props.asyncRunExtension({\n        pathname,\n        extension,\n      });\n    }\n    if (isUndefined(data) && search) {\n      const content = parse(search.slice(1));\n      this.props.changeData({\n        data: content.markdown || '',\n        pathName: this.props.pathname,\n      });\n      this.setState({\n        markdown: (content.markdown as string) || '',\n      });\n      return content.markdown || '';\n    }\n    if (search && !isUndefined(data)) {\n      const content = parse(search.slice(1));\n      if (content.markdown !== this.state.markdown) {\n        this.setState({\n          markdown: (content.markdown as string) || '',\n        });\n        this.props.changeData({\n          data: (content.markdown as string) || '',\n          pathName: this.props.pathname,\n        });\n      }\n    }\n    return data || '';\n  };\n\n  componentDidUpdate = () => {\n    const data = this.checkExtension();\n    if (this.myCodeMirror) {\n      const value = this.myCodeMirror.getValue();\n      if (data !== value) {\n        try {\n          const that = this;\n          setTimeout(() => {\n            that.myCodeMirror.setValue(data);\n            that.myCodeMirror.focus();\n            that.myCodeMirror.setCursor(that.myCodeMirror.lineCount(), 0);\n          }, 10);\n        } catch (_error) {}\n      }\n    }\n  };\n\n  componentDidMount = () => {\n    const data = this.checkExtension();\n    let myTextarea = document.getElementById(editorId) as HTMLTextAreaElement;\n    this.myCodeMirror = HyperMD.fromTextArea(myTextarea, {\n      lineNumbers: false,\n      hmdModeLoader: false,\n    });\n    if (this.myCodeMirror) {\n      const value = this.myCodeMirror.getValue();\n      if (data !== value) {\n        this.myCodeMirror.setValue(data);\n      }\n    }\n    this.myCodeMirror.on('change', (editor: any) => {\n      this.props.changeData({\n        data: editor.getValue(),\n        pathName: this.props.pathname,\n      });\n    });\n    this.myCodeMirror.setSize(800, 621);\n    if (this.props.liveRendering) {\n      HyperMD.switchToHyperMD(this.myCodeMirror);\n    } else {\n      HyperMD.switchToNormal(this.myCodeMirror);\n    }\n  };\n\n  render() {\n    return (\n      <EditorContainer>\n        <textarea id={editorId} />\n      </EditorContainer>\n    );\n  }\n}\n\nexport default connect(mapStateToProps, (dispatch: Dispatch) =>\n  bindActionCreators<typeof useActions, typeof useActions>(useActions, dispatch)\n)(ClipperPluginPage as React.ComponentType<PageProps>);\n"
  },
  {
    "path": "src/pages/plugin/index.less",
    "content": ".mainContent {\n  width: 960px;\n  height: 600px;\n  padding: 10px;\n  background: white;\n  position: relative;\n  box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px;\n  border: 2px solid #dddddd;\n}\n\n.closeIcon {\n  position: absolute;\n  padding: 10px;\n  right: 0;\n  z-index: 100000;\n  top: 0;\n}\n.imageContent {\n  max-width: 960px;\n  max-height: 600px;\n}\n\n:global {\n  .CodeMirror-gutters {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "src/pages/preference/account/index.less",
    "content": ".accountPanel {\n  display: flex;\n  flex-wrap: wrap;\n  width: 100%;\n}\n.createButton {\n  height: 300px;\n}\n"
  },
  {
    "path": "src/pages/preference/account/index.tsx",
    "content": "import * as React from 'react';\nimport {\n  asyncAddAccount,\n  asyncDeleteAccount,\n  asyncUpdateDefaultAccountId,\n  asyncUpdateAccount,\n} from 'pageActions/account';\nimport { PlusOutlined } from '@ant-design/icons';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Button, Row, Col } from 'antd';\nimport { bindActionCreators, Dispatch } from 'redux';\nimport { connect } from 'dva';\nimport AccountItem from '../../../components/accountItem';\nimport styles from './index.less';\nimport EditAccountModal from './modal/editAccountModal';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport CreateAccountModal from './modal/createAccountModal';\nimport { GlobalStore, AccountPreference } from 'common/types';\nimport { FormattedMessage } from 'react-intl';\nimport { asyncChangeAccount } from '@/actions/clipper';\n\nconst useActions = {\n  asyncAddAccount: asyncAddAccount.started,\n  asyncDeleteAccount: asyncDeleteAccount.started,\n  asyncUpdateAccount: asyncUpdateAccount,\n  asyncUpdateDefaultAccountId: asyncUpdateDefaultAccountId.started,\n  asyncChangeAccount: asyncChangeAccount.started,\n};\n\nconst mapStateToProps = ({\n  clipper: { currentAccountId },\n  account: { accounts, defaultAccountId },\n  userPreference: { servicesMeta, imageHostingServicesMeta, imageHosting },\n}: GlobalStore) => {\n  return {\n    currentAccountId,\n    imageHostingServicesMeta,\n    accounts,\n    defaultAccountId,\n    servicesMeta,\n    imageHosting,\n  };\n};\ntype PageState = {\n  showAccountModal: boolean;\n  currentAccount: null | AccountPreference;\n};\n\ntype PageStateProps = ReturnType<typeof mapStateToProps>;\ntype PageDispatchProps = typeof useActions;\ntype PageProps = PageStateProps & PageDispatchProps & FormComponentProps;\nconst mapDispatchToProps = (dispatch: Dispatch) =>\n  bindActionCreators<PageDispatchProps, PageDispatchProps>(useActions, dispatch);\n\nclass Page extends React.Component<PageProps, PageState> {\n  constructor(props: PageProps) {\n    super(props);\n    this.state = {\n      showAccountModal: false,\n      currentAccount: null,\n    };\n  }\n\n  handleSetDefaultId = (id: string) => {\n    if (this.props.defaultAccountId === id) {\n      return;\n    }\n    this.props.asyncUpdateDefaultAccountId({ id });\n  };\n\n  handleEdit = (accountId: string) => {\n    const currentAccount = this.props.accounts.find(o => o.id === accountId);\n    if (!currentAccount) {\n      return;\n    }\n    this.toggleAccountModal(currentAccount);\n  };\n\n  handleAdd = (id: string, userInfo: any) => {\n    const { form } = this.props;\n    form.validateFields((error, values) => {\n      if (error) {\n        return;\n      }\n      const { type, defaultRepositoryId, imageHosting, ...info } = values;\n      this.props.asyncAddAccount({\n        id,\n        type,\n        defaultRepositoryId,\n        imageHosting,\n        info,\n        userInfo,\n        callback: this.handleCancel,\n      });\n    });\n  };\n\n  handleCancel = () => {\n    const { form } = this.props;\n    form.resetFields();\n    this.toggleAccountModal();\n  };\n\n  toggleAccountModal = (currentAccount?: AccountPreference) => {\n    const { showAccountModal } = this.state;\n    this.setState(\n      {\n        showAccountModal: !showAccountModal,\n      },\n      () => {\n        this.setState({\n          currentAccount: currentAccount || null,\n        });\n      }\n    );\n  };\n\n  handleEditAccount = (id: string, userInfo: any, newId: string) => {\n    const { form, asyncUpdateAccount } = this.props;\n    form.validateFields((error, values) => {\n      if (error) {\n        return;\n      }\n      const { type, defaultRepositoryId, imageHosting, ...info } = values;\n      asyncUpdateAccount({\n        account: { type, defaultRepositoryId, imageHosting, info },\n        id,\n        newId,\n        userInfo,\n        callback: () => {\n          this.handleCancel();\n        },\n      });\n    });\n  };\n\n  getAccountModal = () => {\n    const { showAccountModal, currentAccount } = this.state;\n    const { servicesMeta, form, imageHostingServicesMeta, imageHosting } = this.props;\n    const { handleAdd, handleCancel } = this;\n    if (!showAccountModal) {\n      return;\n    }\n    if (currentAccount) {\n      return (\n        <EditAccountModal\n          visible\n          form={form}\n          imageHosting={imageHosting}\n          imageHostingServicesMeta={imageHostingServicesMeta}\n          servicesMeta={servicesMeta}\n          currentAccount={currentAccount}\n          onCancel={this.handleCancel}\n          onEdit={this.handleEditAccount}\n        />\n      );\n    }\n    return (\n      <CreateAccountModal\n        visible\n        form={form}\n        imageHosting={imageHosting}\n        imageHostingServicesMeta={imageHostingServicesMeta}\n        servicesMeta={servicesMeta}\n        onAdd={handleAdd}\n        onCancel={handleCancel}\n      />\n    );\n  };\n\n  render() {\n    const { defaultAccountId, accounts, asyncDeleteAccount, servicesMeta } = this.props;\n    const { handleEdit, handleSetDefaultId, toggleAccountModal } = this;\n    return (\n      <React.Fragment>\n        {this.getAccountModal()}\n        <Row gutter={10}>\n          {accounts.map(account => (\n            <Col span={8} key={account.id} style={{ marginBottom: 10 }}>\n              <AccountItem\n                isDefault={defaultAccountId === account.id}\n                id={account.id}\n                name={account.name}\n                description={account.description}\n                avatar={account.avatar || servicesMeta[account.type].icon}\n                onDelete={id => asyncDeleteAccount({ id })}\n                onEdit={id => handleEdit(id)}\n                onSetDefaultAccount={id => handleSetDefaultId(id)}\n              />\n            </Col>\n          ))}\n          <Col span={8}>\n            <div className={styles.createButton}>\n              <Button\n                type=\"dashed\"\n                onClick={() => toggleAccountModal()}\n                block\n                style={{ height: '100%' }}\n              >\n                <PlusOutlined />\n                <FormattedMessage id=\"preference.account.add\" defaultMessage=\"Bind Account\" />\n              </Button>\n            </div>\n          </Col>\n        </Row>\n      </React.Fragment>\n    );\n  }\n}\n\nexport default connect(mapStateToProps, mapDispatchToProps)(Form.create<PageProps>()(Page));\n"
  },
  {
    "path": "src/pages/preference/account/modal/createAccountModal.tsx",
    "content": "import React, { useMemo } from 'react';\nimport { QuestionCircleOutlined } from '@ant-design/icons';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Modal, Select, Divider } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport styles from './index.less';\nimport { ImageHostingServiceMeta, BUILT_IN_IMAGE_HOSTING_ID } from 'common/backend';\nimport { UserPreferenceStore, ImageHosting } from '@/common/types';\nimport { FormattedMessage } from 'react-intl';\nimport useVerifiedAccount from '@/common/hooks/useVerifiedAccount';\nimport useFilterImageHostingServices from '@/common/hooks/useFilterImageHostingServices';\nimport ImageHostingSelect from '@/components/ImageHostingSelect';\nimport RepositorySelect from '@/components/RepositorySelect';\nimport Container from 'typedi';\nimport { IPermissionsService } from '@/service/common/permissions';\nimport { ITabService } from '@/service/common/tab';\n\ntype PageOwnProps = {\n  imageHostingServicesMeta: {\n    [type: string]: ImageHostingServiceMeta;\n  };\n  servicesMeta: UserPreferenceStore['servicesMeta'];\n  imageHosting: ImageHosting[];\n  visible: boolean;\n  onCancel(): void;\n  onAdd(id: string, userInfo: any): void;\n};\ntype PageProps = PageOwnProps & FormComponentProps;\n\nconst ModalTitle = () => (\n  <div className={styles.modalTitle}>\n    <FormattedMessage id=\"preference.accountList.addAccount\" defaultMessage=\"Add Account\" />\n    <a href={'https://www.yuque.com/yuqueclipper/help_cn/bind_account'} target=\"_blank\">\n      <QuestionCircleOutlined />\n    </a>\n  </div>\n);\n\nconst Page: React.FC<PageProps> = ({\n  imageHosting,\n  imageHostingServicesMeta,\n  servicesMeta,\n  onCancel,\n  form,\n  form: { getFieldDecorator, getFieldValue },\n  onAdd,\n  visible,\n}) => {\n  const {\n    type,\n    accountStatus: { verified, repositories, userInfo, id },\n    loadAccount,\n    verifying,\n    changeType,\n    serviceForm,\n    okText,\n    oauthLink,\n  } = useVerifiedAccount({ form, services: servicesMeta });\n\n  const imageHostingWithBuiltIn = useMemo(() => {\n    const res = [...imageHosting];\n    const meta = imageHostingServicesMeta[type];\n    if (meta?.builtIn) {\n      res.push({ type, info: {}, id: BUILT_IN_IMAGE_HOSTING_ID, remark: meta.builtInRemark });\n    }\n    return res;\n  }, [imageHosting, imageHostingServicesMeta, type]);\n\n  const supportedImageHostingServices = useFilterImageHostingServices({\n    backendServiceType: type,\n    imageHostingServices: imageHostingWithBuiltIn,\n    imageHostingServicesMap: imageHostingServicesMeta,\n  });\n\n  const handleOk = async (e: React.MouseEvent<HTMLElement>) => {\n    e.preventDefault();\n    const type = getFieldValue('type');\n    const permission = servicesMeta[type]?.permission;\n    if (permission) {\n      const result = await Container.get(IPermissionsService).request(permission);\n      if (!result) {\n        return;\n      }\n    }\n    if (oauthLink) {\n      Container.get(ITabService).create({\n        url: oauthLink.props.href,\n      });\n      onCancel();\n    } else if (verified && id) {\n      onAdd(id, userInfo);\n    } else {\n      loadAccount();\n    }\n  };\n\n  return (\n    <Modal\n      visible={visible}\n      okType=\"primary\"\n      onCancel={onCancel}\n      okText={oauthLink ? oauthLink : okText}\n      okButtonProps={{\n        loading: verifying,\n        disabled: verifying,\n      }}\n      onOk={handleOk}\n      title={<ModalTitle />}\n    >\n      <Form labelCol={{ span: 7, offset: 0 }} wrapperCol={{ span: 17 }}>\n        <Form.Item\n          label={<FormattedMessage id=\"preference.accountList.type\" defaultMessage=\"Type\" />}\n        >\n          {getFieldDecorator('type', {\n            initialValue: type,\n          })(\n            <Select showSearch disabled={verified} onChange={changeType}>\n              {Object.values(servicesMeta).map(o => (\n                <Select.Option key={o.type} label={o.name} value={o.type}>\n                  {o.name}\n                </Select.Option>\n              ))}\n            </Select>\n          )}\n        </Form.Item>\n        {!oauthLink && serviceForm}\n        {!oauthLink && (\n          <React.Fragment>\n            <Divider />\n            <Form.Item\n              label={\n                <FormattedMessage\n                  id=\"preference.accountList.defaultRepository\"\n                  defaultMessage=\"Default Repository\"\n                />\n              }\n            >\n              {getFieldDecorator('defaultRepositoryId')(\n                <RepositorySelect\n                  disabled={!verified}\n                  loading={verifying}\n                  repositories={repositories}\n                />\n              )}\n            </Form.Item>\n            <Form.Item\n              label={\n                <FormattedMessage\n                  id=\"preference.accountList.imageHost\"\n                  defaultMessage=\"Image Host\"\n                />\n              }\n            >\n              {getFieldDecorator('imageHosting')(\n                <ImageHostingSelect\n                  loading={verifying}\n                  disabled={!verified}\n                  supportedImageHostingServices={supportedImageHostingServices}\n                />\n              )}\n            </Form.Item>\n          </React.Fragment>\n        )}\n      </Form>\n    </Modal>\n  );\n};\n\nexport default Page;\n"
  },
  {
    "path": "src/pages/preference/account/modal/editAccountModal.tsx",
    "content": "import React, { useMemo, useEffect } from 'react';\nimport { QuestionCircleOutlined } from '@ant-design/icons';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Modal, Select } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport styles from './index.less';\nimport { ImageHostingServiceMeta } from 'common/backend';\nimport { AccountPreference, UserPreferenceStore, ImageHosting } from '@/common/types';\nimport { FormattedMessage } from 'react-intl';\nimport ImageHostingSelect from '@/components/ImageHostingSelect';\nimport useFilterImageHostingServices from '@/common/hooks/useFilterImageHostingServices';\nimport useVerifiedAccount from '@/common/hooks/useVerifiedAccount';\nimport RepositorySelect from '@/components/RepositorySelect';\nimport { BUILT_IN_IMAGE_HOSTING_ID } from '@/common/backend/imageHosting/interface';\n\ntype PageOwnProps = {\n  imageHostingServicesMeta: {\n    [type: string]: ImageHostingServiceMeta;\n  };\n  servicesMeta: UserPreferenceStore['servicesMeta'];\n  imageHosting: ImageHosting[];\n  currentAccount: AccountPreference;\n  visible: boolean;\n  onCancel(): void;\n  onEdit(oldId: string, userInfo: any, newId: string): void;\n};\ntype PageProps = PageOwnProps & FormComponentProps;\n\nconst ModalTitle = () => (\n  <div className={styles.modalTitle}>\n    <FormattedMessage id=\"preference.accountList.addAccount\" defaultMessage=\"Add Account\" />\n    <a href={'https://www.yuque.com/yuqueclipper/help_cn/bind_account'} target=\"_blank\">\n      <QuestionCircleOutlined />\n    </a>\n  </div>\n);\n\nconst Page: React.FC<PageProps> = ({\n  visible,\n  currentAccount,\n  servicesMeta,\n  form,\n  form: { getFieldDecorator },\n  onCancel,\n  onEdit,\n  imageHosting,\n  imageHostingServicesMeta,\n}) => {\n  const {\n    type,\n    accountStatus: { verified, repositories, userInfo, id },\n    verifyAccount,\n    loadAccount,\n    serviceForm,\n    verifying,\n    okText: verifyText,\n  } = useVerifiedAccount({\n    form,\n    services: servicesMeta,\n    initAccount: currentAccount,\n  });\n\n  useEffect(() => {\n    verifyAccount(currentAccount);\n  }, [currentAccount, verifyAccount]);\n\n  const imageHostingWithBuiltIn = useMemo(() => {\n    const res = [...imageHosting];\n    const meta = imageHostingServicesMeta[type];\n    if (meta?.builtIn) {\n      res.push({ type, info: {}, id: BUILT_IN_IMAGE_HOSTING_ID, remark: meta.builtInRemark });\n    }\n    return res;\n  }, [imageHosting, imageHostingServicesMeta, type]);\n\n  const supportedImageHostingServices = useFilterImageHostingServices({\n    backendServiceType: currentAccount.type,\n    imageHostingServices: imageHostingWithBuiltIn,\n    imageHostingServicesMap: imageHostingServicesMeta,\n  });\n\n  const okText = verifying ? (\n    <FormattedMessage id=\"preference.accountList.verifying\" defaultMessage=\"Verifying\" />\n  ) : (\n    <FormattedMessage id=\"preference.accountList.confirm\" defaultMessage=\"Confirm\" />\n  );\n\n  return (\n    <Modal\n      visible={visible}\n      title={<ModalTitle />}\n      okText={verified ? okText : verifyText}\n      okType=\"primary\"\n      okButtonProps={{\n        loading: verifying,\n      }}\n      onCancel={onCancel}\n      onOk={() => {\n        if (verified) {\n          onEdit(currentAccount.id, userInfo, id!);\n        } else {\n          loadAccount();\n        }\n      }}\n    >\n      <Form labelCol={{ span: 7, offset: 0 }} wrapperCol={{ span: 17 }}>\n        <Form.Item\n          label={<FormattedMessage id=\"preference.accountList.type\" defaultMessage=\"Type\" />}\n        >\n          {getFieldDecorator('type', {\n            initialValue: currentAccount.type,\n          })(\n            <Select disabled>\n              {Object.values(servicesMeta).map(o => (\n                <Select.Option key={o.type} value={o.type}>\n                  {o.name}\n                </Select.Option>\n              ))}\n            </Select>\n          )}\n        </Form.Item>\n        {serviceForm}\n        <Form.Item\n          label={\n            <FormattedMessage\n              id=\"preference.accountList.defaultRepository\"\n              defaultMessage=\"Default Repository\"\n            />\n          }\n        >\n          {getFieldDecorator('defaultRepositoryId', {\n            initialValue: currentAccount.defaultRepositoryId,\n          })(\n            <RepositorySelect\n              disabled={!verified || verifying}\n              loading={verifying}\n              repositories={repositories}\n            />\n          )}\n        </Form.Item>\n        <Form.Item\n          label={\n            <FormattedMessage id=\"preference.accountList.imageHost\" defaultMessage=\"Image Host\" />\n          }\n        >\n          {getFieldDecorator('imageHosting', {\n            initialValue: currentAccount.imageHosting,\n          })(\n            <ImageHostingSelect\n              disabled={!verified}\n              supportedImageHostingServices={supportedImageHostingServices}\n            ></ImageHostingSelect>\n          )}\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default Page;\n"
  },
  {
    "path": "src/pages/preference/account/modal/index.less",
    "content": ".modalTitle {\n  a {\n    margin-left: 10px;\n  }\n}\n"
  },
  {
    "path": "src/pages/preference/base.tsx",
    "content": "import React from 'react';\nimport { GlobalStore, DvaRouterProps } from '@/common/types';\nimport { connect } from 'dva';\nimport { List, Select, Switch } from 'antd';\nimport { asyncSetLocaleToStorage, asyncSetEditorLiveRendering } from '@/actions/userPreference';\nimport { FormattedMessage } from 'react-intl';\nimport { locales } from '@/common/locales';\nimport { useObserver } from 'mobx-react';\nimport Container from 'typedi';\nimport { IConfigService } from '@/service/common/config';\nimport { IPreferenceService } from '@/service/common/preference';\n\nconst mapStateToProps = ({ userPreference: { locale, liveRendering, iconColor } }: GlobalStore) => {\n  return {\n    locale,\n    liveRendering,\n    iconColor,\n  };\n};\ntype PageStateProps = ReturnType<typeof mapStateToProps>;\n\ntype PageProps = PageStateProps & DvaRouterProps;\n\nconst Base: React.FC<PageProps> = (props) => {\n  const { dispatch } = props;\n\n  const { iconColor, preferenceService } = useObserver(() => {\n    const preferenceService = Container.get(IPreferenceService);\n    return {\n      preferenceService,\n      iconColor: preferenceService.userPreference.iconColor,\n    } as const;\n  });\n\n  const originConfigs = [\n    {\n      key: 'configLanguage',\n      action: (\n        <Select\n          key=\"configLanguage\"\n          value={props.locale}\n          onChange={(e: string) => dispatch(asyncSetLocaleToStorage(e))}\n          dropdownMatchSelectWidth={false}\n        >\n          {locales.map((o) => (\n            <Select.Option key={o.locale} value={o.locale}>\n              {o.name}\n            </Select.Option>\n          ))}\n        </Select>\n      ),\n      title: (\n        <FormattedMessage id=\"preference.basic.configLanguage.title\" defaultMessage=\"Language\" />\n      ),\n      description: (\n        <FormattedMessage\n          id=\"preference.basic.configLanguage.description\"\n          defaultMessage=\"My native language is Chinese,Welcome to submit a translation on GitHub\"\n          values={{\n            GitHub: (\n              <a\n                href=\"https://github.com/webclipper/web-clipper/tree/master/src/common/locales/data\"\n                target=\"_blank\"\n              >\n                GitHub\n              </a>\n            ),\n          }}\n        />\n      ),\n    },\n    {\n      key: 'iconColor',\n      action: (\n        <Select\n          key=\"configLanguage\"\n          value={iconColor}\n          dropdownMatchSelectWidth={false}\n          onChange={preferenceService.updateIconColor}\n        >\n          {[\n            {\n              name: <FormattedMessage id=\"preference.basic.iconColor.dark\" />,\n              value: 'dark',\n            },\n            {\n              name: <FormattedMessage id=\"preference.basic.iconColor.auto\" />,\n              value: 'auto',\n            },\n            {\n              name: <FormattedMessage id=\"preference.basic.iconColor.light\" />,\n              value: 'light',\n            },\n          ].map((o) => (\n            <Select.Option key={o.value} value={o.value}>\n              {o.name}\n            </Select.Option>\n          ))}\n        </Select>\n      ),\n      title: <FormattedMessage id=\"preference.basic.iconColor.title\" defaultMessage=\"Icon Color\" />,\n      description: (\n        <FormattedMessage id=\"preference.basic.iconColor.description\" defaultMessage=\"Icon Color\" />\n      ),\n    },\n    {\n      key: 'liveRendering',\n      action: (\n        <Switch\n          key=\"liveRendering\"\n          checked={props.liveRendering}\n          onChange={() => {\n            dispatch(\n              asyncSetEditorLiveRendering.started({\n                value: props.liveRendering,\n              })\n            );\n          }}\n        />\n      ),\n      title: (\n        <FormattedMessage\n          id=\"preference.basic.liveRendering.title\"\n          defaultMessage=\"LiveRendering\"\n        />\n      ),\n      description: (\n        <FormattedMessage\n          id=\"preference.basic.liveRendering.description\"\n          defaultMessage=\"Enable LiveRendering\"\n        />\n      ),\n    },\n  ];\n\n  const configService = Container.get(IConfigService);\n\n  const configs = useObserver(() => {\n    if (configService.isLatestVersion) {\n      return originConfigs;\n    }\n    return originConfigs.concat({\n      key: 'update',\n      action: (\n        <a href=\"https://github.com/webclipper/web-clipper/releases\" target=\"_blank\">\n          <FormattedMessage id=\"preference.basic.update.button\" defaultMessage=\"Install Update\" />\n        </a>\n      ),\n      title: <FormattedMessage id=\"preference.basic.update.title\" defaultMessage=\"Has Update\" />,\n      description: (\n        <FormattedMessage\n          id=\"preference.basic.update.description\"\n          defaultMessage=\"Because the review takes a week, the chrome version will fall behind.\"\n        />\n      ),\n    });\n  });\n\n  return (\n    <React.Fragment>\n      {configs.map(({ key, action, title, description }) => (\n        <List.Item key={key} actions={[action]}>\n          <List.Item.Meta title={title} description={description} />\n        </List.Item>\n      ))}\n    </React.Fragment>\n  );\n};\n\nexport default connect(mapStateToProps)(Base as React.FC<PageProps>) as any;\n"
  },
  {
    "path": "src/pages/preference/changelog/index.tsx",
    "content": "import React from 'react';\nimport { Skeleton } from 'antd';\nimport ReactMarkdown from 'react-markdown';\nimport Container from 'typedi';\nimport { useFetch } from '@shihengtech/hooks';\nimport LinkRender from '@/components/LinkRender';\nimport { IEnvironmentService } from '@/services/environment/common/environment';\n\nconst Changelog: React.FC = () => {\n  const environmentService = Container.get(IEnvironmentService);\n  const { loading, data: changelog } = useFetch(async () => {\n    return environmentService.changelog();\n  }, []);\n\n  if (loading || !changelog) {\n    return <Skeleton active />;\n  }\n  return <ReactMarkdown components={{ a: LinkRender } as any}>{changelog}</ReactMarkdown>;\n};\n\nexport default Changelog;\n"
  },
  {
    "path": "src/pages/preference/extensions/index.less",
    "content": ".extensionCard {\n  margin-bottom: 20px;\n}\n"
  },
  {
    "path": "src/pages/preference/extensions/index.tsx",
    "content": "import React from 'react';\nimport { QuestionCircleOutlined, StarOutlined } from '@ant-design/icons';\nimport { Row, Col, Typography, Tooltip, Empty, Switch } from 'antd';\nimport useFilterExtensions from '@/common/hooks/useFilterExtensions';\nimport { FormattedMessage } from 'react-intl';\nimport ExtensionCard from '@/components/ExtensionCard';\nimport styles from './index.less';\nimport { ExtensionType, IExtensionWithId } from '@/extensions/common';\nimport IconFont from '@/components/IconFont';\nimport Container from 'typedi';\nimport { IExtensionService, IExtensionContainer } from '@/service/common/extension';\nimport { useObserver } from 'mobx-react';\n\nconst Page: React.FC = () => {\n  const extensionService = Container.get(IExtensionService);\n  const extensionContainer = Container.get(IExtensionContainer);\n  const {\n    disabledExtensions,\n    enabledAutomaticExtensionIds,\n    defaultExtensionId,\n    extensions,\n    contextMenus,\n  } = useObserver(() => {\n    return {\n      defaultExtensionId: extensionService.DefaultExtensionId,\n      enabledAutomaticExtensionIds: extensionService.EnabledAutomaticExtensionIds,\n      disabledExtensions: extensionService.DisabledExtensionIds,\n      extensions: extensionContainer.extensions,\n      contextMenus: extensionContainer.contextMenus,\n    };\n  });\n  const [toolExtensions, clipExtensions] = useFilterExtensions(extensions);\n  const handleSetDefault = (extensionId: string) => {\n    extensionService.toggleDefault(extensionId);\n  };\n  const cardActions = (e: IExtensionWithId) => {\n    const actions = [];\n    if (e.type !== ExtensionType.Tool) {\n      const isDefaultExtension = defaultExtensionId === e.id;\n      const iconStyle = isDefaultExtension ? { color: 'red' } : {};\n      const title = isDefaultExtension ? (\n        <FormattedMessage\n          id=\"preference.extensions.CancelSetting\"\n          defaultMessage=\"Cancel Setting\"\n        />\n      ) : (\n        <FormattedMessage\n          id=\"preference.extensions.ConfiguredAsDefaultExtension\"\n          defaultMessage=\"Configured as default extension\"\n        />\n      );\n      actions.push(\n        <Tooltip title={title}>\n          <StarOutlined style={iconStyle} onClick={() => handleSetDefault(e.id)} />\n        </Tooltip>\n      );\n    }\n    if (e.manifest.automatic) {\n      const automaticDisabled = enabledAutomaticExtensionIds.every(o => o !== e.id);\n      actions.push(\n        <Tooltip\n          title={\n            automaticDisabled ? (\n              <FormattedMessage\n                id=\"preference.extensions.automaticOperationIsProhibited\"\n                defaultMessage=\"Automatic operation is prohibited\"\n              />\n            ) : (\n              <FormattedMessage id=\"preference.extensions.runAutomaticOnSaving\" />\n            )\n          }\n        >\n          <IconFont\n            type=\"auto\"\n            onClick={() => extensionService.toggleAutomaticExtension(e.id)}\n            style={automaticDisabled ? {} : { color: 'red' }}\n          />\n        </Tooltip>\n      );\n    }\n    return actions.concat(\n      <Switch\n        size=\"small\"\n        checked={!disabledExtensions.some(o => o === e.id)}\n        onClick={() => extensionService.toggleDisableExtension(e.id)}\n      />\n    );\n  };\n\n  return (\n    <div>\n      <Typography.Title level={3}>\n        <FormattedMessage id=\"preference.extensions.contextMenus\" />\n      </Typography.Title>\n      <Row gutter={10}>\n        {contextMenus.length === 0 && <Empty></Empty>}\n        {contextMenus.map(e => {\n          const Factory = e.contextMenu;\n          const contextMenus = new Factory();\n          return (\n            <Col key={e.id} span={12}>\n              <ExtensionCard\n                className={styles.extensionCard}\n                manifest={contextMenus.manifest}\n                actions={[\n                  <Switch\n                    key=\"toggle\"\n                    size=\"small\"\n                    checked={!disabledExtensions.some(o => o === e.id)}\n                    onClick={() => extensionService.toggleDisableExtension(e.id)}\n                  />,\n                ]}\n              ></ExtensionCard>\n            </Col>\n          );\n        })}\n      </Row>\n      <Typography.Title level={3}>\n        <FormattedMessage\n          id=\"preference.extensions.toolExtensions\"\n          defaultMessage=\"Tool Extensions\"\n        />\n      </Typography.Title>\n      <Row gutter={10}>\n        {toolExtensions.length === 0 && <Empty></Empty>}\n        {toolExtensions.map(e => (\n          <Col key={e.id} span={12}>\n            <ExtensionCard\n              className={styles.extensionCard}\n              manifest={e.manifest}\n              actions={cardActions(e)}\n            ></ExtensionCard>\n          </Col>\n        ))}\n      </Row>\n      <Typography.Title level={3}>\n        <FormattedMessage\n          id=\"preference.extensions.clipExtensions\"\n          defaultMessage=\"Clip Extensions\"\n        />\n        <Tooltip\n          title={\n            <FormattedMessage\n              id=\"preference.extensions.clipExtensions.tooltip\"\n              defaultMessage=\"Click on the 🌟 to choose the default extension.\"\n            />\n          }\n        >\n          <QuestionCircleOutlined style={{ fontSize: 14, marginLeft: 5 }} />\n        </Tooltip>\n      </Typography.Title>\n      <Row gutter={8}>\n        {clipExtensions.map(e => (\n          <Col key={e.id} span={12}>\n            <ExtensionCard\n              className={styles.extensionCard}\n              manifest={e.manifest}\n              actions={cardActions(e)}\n            ></ExtensionCard>\n          </Col>\n        ))}\n      </Row>\n    </div>\n  );\n};\n\nexport default Page;\n"
  },
  {
    "path": "src/pages/preference/imageHosting/form/addImageHosting.tsx",
    "content": "import React from 'react';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Modal, Select, Input } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport { ImageHostingServiceMeta } from '../../../../common/backend';\nimport { ImageHosting } from '@/common/types';\nimport { FormattedMessage } from 'react-intl';\nimport Container from 'typedi';\nimport { IPermissionsService } from '@/service/common/permissions';\n\ntype PageOwnProps = {\n  currentImageHosting?: ImageHosting | null;\n  imageHostingServicesMeta: { [type: string]: ImageHostingServiceMeta };\n  visible: boolean;\n  onAddAccount(): void;\n  onEditAccount(id: string): void;\n  onCancel(): void;\n};\n\ntype PageProps = PageOwnProps & FormComponentProps;\n\nconst formItemLayout = {\n  wrapperCol: { span: 17 },\n  labelCol: { span: 6, offset: 0 },\n};\n\nconst AddImageHostingModal: React.FC<PageProps> = props => {\n  const {\n    imageHostingServicesMeta,\n    visible,\n    currentImageHosting,\n    form: { getFieldDecorator, getFieldValue },\n  } = props;\n\n  const getImageHostingForm = (info?: Pick<ImageHosting, 'info'>) => {\n    const {\n      imageHostingServicesMeta,\n      form: { getFieldValue },\n      form,\n    } = props;\n    const type = getFieldValue('type');\n    if (type) {\n      const ServiceForm = imageHostingServicesMeta[type]?.form;\n      if (ServiceForm) {\n        return <ServiceForm form={form} info={info} />;\n      }\n    }\n  };\n\n  const handleOk = async () => {\n    const permissionsService = Container.get(IPermissionsService);\n    const type = getFieldValue('type');\n    const permission = imageHostingServicesMeta[type]?.permission;\n    if (permission) {\n      const result = await permissionsService.request(permission);\n      if (!result) {\n        return;\n      }\n    }\n    const { currentImageHosting } = props;\n    if (currentImageHosting) {\n      props.onEditAccount(currentImageHosting.id);\n    } else {\n      props.onAddAccount();\n    }\n  };\n\n  const services = Object.values(imageHostingServicesMeta);\n  let title;\n  let initImageHosting: Omit<ImageHosting, 'id'>;\n  if (currentImageHosting) {\n    title = <FormattedMessage id=\"preference.imageHosting.edit\" defaultMessage=\"Edit\" />;\n    initImageHosting = currentImageHosting;\n  } else {\n    title = <FormattedMessage id=\"preference.imageHosting.add\" defaultMessage=\"Add\" />;\n    initImageHosting = {\n      type: services.filter(o => !o.builtIn)[0].type,\n    };\n  }\n\n  return (\n    <Modal title={title} visible={visible} onOk={handleOk} onCancel={props.onCancel} destroyOnClose>\n      <Form {...formItemLayout}>\n        <Form.Item\n          label={<FormattedMessage id=\"preference.imageHosting.type\" defaultMessage=\"Type\" />}\n        >\n          {getFieldDecorator('type', {\n            initialValue: initImageHosting.type,\n            rules: [{ required: true }],\n          })(\n            <Select>\n              {services\n                .filter(o => !o.builtIn)\n                .map(service => (\n                  <Select.Option key={service.type} value={service.type}>\n                    {service.name}\n                  </Select.Option>\n                ))}\n            </Select>\n          )}\n        </Form.Item>\n        {getImageHostingForm(initImageHosting.info)}\n        <Form.Item\n          label={<FormattedMessage id=\"preference.imageHosting.remark\" defaultMessage=\"Remark\" />}\n        >\n          {getFieldDecorator('remark', {\n            initialValue: initImageHosting.remark,\n          })(<Input />)}\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n};\n\nexport default AddImageHostingModal;\n"
  },
  {
    "path": "src/pages/preference/imageHosting/index.less",
    "content": ".listItem {\n  padding-top: 16px;\n  padding-bottom: 16px;\n  display: flex;\n  align-items: center;\n  .icon {\n    width: 48px;\n    height: 48px;\n    padding: 10;\n    font-size: 48px;\n  }\n  .actionSplit {\n    position: absolute;\n    top: 50%;\n    right: 0;\n    width: 1px;\n    height: 14px;\n    margin-top: -7px;\n    background-color: #e8e8e8;\n  }\n}\n.box {\n  padding: 10px;\n}\n"
  },
  {
    "path": "src/pages/preference/imageHosting/index.tsx",
    "content": "import * as React from 'react';\nimport {\n  asyncAddImageHosting,\n  asyncDeleteImageHosting,\n  asyncEditImageHosting,\n} from 'pageActions/userPreference';\nimport { bindActionCreators, Dispatch } from 'redux';\nimport { connect } from 'dva';\nimport styles from './index.less';\nimport AddImageHosting from './form/addImageHosting';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport ImageHostingListItem from 'components/imagehostingListItem';\nimport { PlusOutlined } from '@ant-design/icons';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Button } from 'antd';\nimport { GlobalStore, ImageHosting } from 'common/types';\nimport { FormattedMessage } from 'react-intl';\nimport { asyncDeleteAccount } from '@/actions/account';\n\nconst useActions = {\n  asyncAddImageHosting: asyncAddImageHosting.started,\n  asyncDeleteAccount: asyncDeleteAccount.started,\n  asyncDeleteImageHosting: asyncDeleteImageHosting.started,\n  asyncEditImageHosting: asyncEditImageHosting.started,\n};\n\nconst mapStateToProps = ({\n  userPreference: { imageHostingServicesMeta, imageHosting },\n}: GlobalStore) => {\n  return {\n    imageHostingServicesMeta,\n    imageHosting,\n  };\n};\ntype PageState = {\n  showAddImageHostingModal: boolean;\n  currentImageHosting?: null | ImageHosting;\n};\n\ntype PageStateProps = ReturnType<typeof mapStateToProps>;\ntype PageDispatchProps = typeof useActions;\ntype PageOwnProps = {};\ntype PageProps = PageStateProps & PageDispatchProps & PageOwnProps & FormComponentProps;\nconst mapDispatchToProps = (dispatch: Dispatch) =>\n  bindActionCreators<PageDispatchProps, PageDispatchProps>(useActions, dispatch);\n\nclass Page extends React.Component<PageProps, PageState> {\n  constructor(props: PageProps) {\n    super(props);\n    this.state = {\n      showAddImageHostingModal: false,\n      currentImageHosting: null,\n    };\n  }\n\n  handleAddAccount = () => {\n    this.props.form.validateFields((error, values) => {\n      if (error) {\n        return;\n      }\n      const { type, remark, ...info } = values;\n      this.props.asyncAddImageHosting({\n        type,\n        remark,\n        info,\n        closeModal: this.closeModalAndResetForm,\n      });\n    });\n  };\n\n  closeModalAndResetForm = () => {\n    this.setState(\n      {\n        currentImageHosting: null,\n        showAddImageHostingModal: false,\n      },\n      () => this.props.form.resetFields()\n    );\n  };\n\n  handleEditAccount = (id: string) => {\n    this.props.form.validateFields((error, values) => {\n      if (error) {\n        return;\n      }\n      const { type, remark, ...info } = values;\n      this.props.asyncEditImageHosting({\n        id,\n        value: { type, remark, info },\n        closeModal: this.closeModalAndResetForm,\n      });\n    });\n  };\n\n  handleStartAddAccount = () => {\n    this.setState({\n      showAddImageHostingModal: true,\n    });\n  };\n\n  handleDeleteImageHosting = (id: string) => {\n    this.props.asyncDeleteImageHosting({ id });\n  };\n\n  handleEditImageHosting = (id: string) => {\n    const { imageHosting } = this.props;\n    this.setState({\n      showAddImageHostingModal: true,\n      currentImageHosting: imageHosting.find(o => o.id === id),\n    });\n  };\n\n  renderImageHosting = () => {\n    const { imageHosting, imageHostingServicesMeta } = this.props;\n    return imageHosting\n      .filter(o => imageHostingServicesMeta[o.type])\n      .map(o => {\n        const meta = imageHostingServicesMeta[o.type];\n        return (\n          <ImageHostingListItem\n            id={o.id}\n            key={o.id}\n            name={meta.name}\n            icon={meta.icon}\n            remark={o.remark}\n            onEditAccount={id => this.handleEditImageHosting(id)}\n            onDeleteAccount={id => this.handleDeleteImageHosting(id)}\n          />\n        );\n      });\n  };\n\n  render() {\n    const { form, imageHostingServicesMeta } = this.props;\n    const { showAddImageHostingModal, currentImageHosting } = this.state;\n\n    return (\n      <div className={styles.box}>\n        <AddImageHosting\n          currentImageHosting={currentImageHosting}\n          imageHostingServicesMeta={imageHostingServicesMeta as any}\n          visible={showAddImageHostingModal}\n          form={form}\n          onCancel={this.closeModalAndResetForm}\n          onAddAccount={this.handleAddAccount}\n          onEditAccount={this.handleEditAccount}\n        />\n        <Button\n          type=\"dashed\"\n          onClick={this.handleStartAddAccount}\n          style={{ height: 30, marginBottom: 10, width: '100%' }}\n        >\n          <PlusOutlined />\n          <FormattedMessage id=\"preference.imageHosting.add\" defaultMessage=\"Add\" />\n        </Button>\n        {this.renderImageHosting()}\n      </div>\n    );\n  }\n}\n\nexport default connect(\n  mapStateToProps,\n  mapDispatchToProps\n  //@ts-ignore\n)(Form.create<PageProps>()(Page));\n"
  },
  {
    "path": "src/pages/preference/index.less",
    "content": ".mainContent {\n  width: 960px;\n  height: 600px;\n  padding: 8px;\n  background: white;\n  position: relative;\n  box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px;\n  border: 2px solid #dddddd;\n  :global {\n    .ant-tabs-content {\n      height: 100%;\n    }\n  }\n}\n\n.closeIcon {\n  position: absolute;\n  padding: 10px;\n  right: 0;\n  top: 0;\n  z-index: 10000;\n  cursor: pointer;\n}\n\n.tabPane {\n  padding: 20px;\n  height: 100%;\n  overflow-y: scroll;\n}\n"
  },
  {
    "path": "src/pages/preference/index.tsx",
    "content": "import * as React from 'react';\nimport styles from './index.less';\nimport Account from './account';\nimport ImageHosting from './imageHosting';\nimport Extensions from './extensions';\nimport { CenterContainer } from 'components/container';\nimport { router, connect } from 'dva';\n\nimport {\n  CloseOutlined,\n  PictureOutlined,\n  ToolOutlined,\n  UserOutlined,\n  SettingOutlined,\n} from '@ant-design/icons';\n\nimport { Tabs, Badge, message } from 'antd';\nimport { FormattedMessage } from 'react-intl';\nimport Base from './base';\nimport { DvaRouterProps, GlobalStore } from '@/common/types';\nimport Changelog from './changelog';\nimport IconFont from '@/components/IconFont';\nimport Privacy from './privacy';\nimport locale from '@/common/locales';\nimport Container from 'typedi';\nimport { IConfigService } from '@/service/common/config';\nimport { useObserver } from 'mobx-react';\n\nconst { Route } = router;\n\nconst TabPane = Tabs.TabPane;\n\nconst mapStateToProps = ({ account: { accounts } }: GlobalStore) => {\n  return {\n    accounts,\n  };\n};\ntype PageStateProps = ReturnType<typeof mapStateToProps>;\n\nconst tabs = [\n  {\n    path: 'account',\n    icon: <UserOutlined />,\n    title: <FormattedMessage id=\"preference.tab.account\" defaultMessage=\"Account\" />,\n    component: Account,\n  },\n  {\n    path: 'extensions',\n\n    icon: <ToolOutlined />,\n    title: <FormattedMessage id=\"preference.tab.extensions\" defaultMessage=\"Extension\" />,\n    component: Extensions,\n  },\n  {\n    path: 'imageHost',\n    icon: <PictureOutlined />,\n\n    title: <FormattedMessage id=\"preference.tab.imageHost\" defaultMessage=\"ImageHost\" />,\n    component: ImageHosting,\n  },\n  {\n    path: 'base',\n    icon: <SettingOutlined />,\n    title: <FormattedMessage id=\"preference.tab.basic\" defaultMessage=\"Basic\" />,\n    component: Base,\n  },\n  {\n    path: 'privacy',\n    icon: <IconFont type=\"privacy\" />,\n    title: <FormattedMessage id=\"preference.tab.privacy\" defaultMessage=\"Privacy policy\" />,\n    component: Privacy,\n  },\n  {\n    path: 'changelog',\n    icon: <IconFont type=\"changelog\" />,\n    title: <FormattedMessage id=\"preference.tab.changelog\" defaultMessage=\"Changelog\" />,\n    component: Changelog,\n  },\n];\n\ntype PageProps = DvaRouterProps & PageStateProps;\n\nconst Preference: React.FC<PageProps> = ({\n  location: { pathname },\n  history: { push },\n  accounts,\n}) => {\n  const goHome = () => {\n    if (accounts.length === 0) {\n      message.error(\n        locale.format({\n          id: 'preference.bind.message',\n          defaultMessage: 'You need to bind an account before you can use it.',\n        })\n      );\n      return;\n    }\n    push('/');\n  };\n\n  const configService = Container.get(IConfigService);\n\n  const isLatestVersion = useObserver(() => configService.isLatestVersion);\n\n  return (\n    <CenterContainer>\n      <div className={styles.mainContent}>\n        <div onClick={goHome} className={styles.closeIcon}>\n          <CloseOutlined />\n        </div>\n        <div style={{ background: 'white', height: '100%' }}>\n          <Tabs activeKey={pathname} tabPosition=\"left\" style={{ height: '100%' }} onChange={push}>\n            {tabs.map(tab => {\n              const path = `/preference/${tab.path}`;\n              let tabTitle = (\n                <div style={{ width: 100 }}>\n                  {tab.icon}\n                  {tab.title}\n                </div>\n              );\n              if (!isLatestVersion && tab.path === 'base') {\n                tabTitle = <Badge dot>{tabTitle}</Badge>;\n              }\n              return (\n                <TabPane tab={tabTitle} key={path} className={styles.tabPane}>\n                  <Route exact path={path} component={tab.component} />\n                </TabPane>\n              );\n            })}\n          </Tabs>\n        </div>\n      </div>\n    </CenterContainer>\n  );\n};\n\nexport default connect(mapStateToProps)(Preference);\n"
  },
  {
    "path": "src/pages/preference/privacy/index.tsx",
    "content": "import React from 'react';\nimport { Skeleton } from 'antd';\nimport ReactMarkdown from 'react-markdown';\nimport LinkRender from '@/components/LinkRender';\nimport Container from 'typedi';\nimport { useFetch } from '@shihengtech/hooks';\nimport { IEnvironmentService } from '@/services/environment/common/environment';\n\nconst Privacy: React.FC = () => {\n  const environmentService = Container.get(IEnvironmentService);\n  const { loading, data: privacy } = useFetch(async () => {\n    return environmentService.privacy();\n  }, []);\n\n  if (loading || !privacy) {\n    return <Skeleton active />;\n  }\n  return <ReactMarkdown components={{ a: LinkRender } as any}>{privacy}</ReactMarkdown>;\n};\n\nexport default Privacy;\n"
  },
  {
    "path": "src/pages/tool/ClipExtension.tsx",
    "content": "import * as React from 'react';\nimport styles from './index.less';\nimport { Button } from 'antd';\nimport Section from 'components/section';\nimport { FormattedMessage } from 'react-intl';\nimport IconFont from '@/components/IconFont';\nimport { IExtensionWithId } from '@/extensions/common';\nimport localeService from '@/common/locales';\n\ntype PageProps = {\n  hasEditor: boolean;\n  extensions: IExtensionWithId[];\n  pathname: string;\n  onClick(router: string): void;\n};\n\nconst ClipExtensions: React.FC<PageProps> = ({ extensions, pathname, onClick, hasEditor }) => {\n  const handleClick = (pluginRouter: string) => {\n    if (pluginRouter !== pathname) {\n      onClick(pluginRouter);\n    }\n  };\n  return (\n    <Section\n      className={styles.section}\n      title={<FormattedMessage id=\"tool.clipExtensions\" defaultMessage=\"Clip Extensions\" />}\n    >\n      {extensions.map(plugin => {\n        const useThisPlugin = plugin.router === pathname;\n        const buttonStyle = useThisPlugin ? { color: '#40a9ff' } : {};\n        return (\n          <Button\n            title={plugin.manifest.description}\n            block\n            key={plugin.id}\n            className={styles.menuButton}\n            style={buttonStyle}\n            onClick={() => handleClick(plugin.router)}\n          >\n            <IconFont type={plugin.manifest.icon} />\n            {plugin.manifest.name}\n          </Button>\n        );\n      })}\n      {hasEditor && (\n        <Button\n          block\n          key={'/editor'}\n          className={styles.menuButton}\n          style={pathname === '/editor' ? { color: '#40a9ff' } : {}}\n          onClick={() => handleClick('/editor')}\n        >\n          <IconFont type={'select'} />\n          {localeService.format({ id: 'contextMenus.selection.save.title' })}\n        </Button>\n      )}\n    </Section>\n  );\n};\n\nexport default ClipExtensions;\n"
  },
  {
    "path": "src/pages/tool/Header.tsx",
    "content": "import React, { useEffect, useRef, useMemo } from 'react';\nimport { Form } from '@ant-design/compatible';\nimport '@ant-design/compatible/assets/index.less';\nimport { Input, Button } from 'antd';\nimport { FormComponentProps } from '@ant-design/compatible/lib/form';\nimport Section from '@/components/section';\nimport { FormattedMessage } from 'react-intl';\nimport styles from './index.less';\nimport { useSelector, useDispatch } from 'dva';\nimport { GlobalStore, ClipperHeaderForm } from '@/common/types';\nimport { updateClipperHeader, asyncCreateDocument } from '@/actions/clipper';\nimport { isEqual } from 'lodash';\nimport { ServiceMeta, Repository } from '@/common/backend';\nimport classNames from 'classnames';\nimport localeService from '@/common/locales';\n\ntype PageProps = FormComponentProps & {\n  pathname: string;\n  service: ServiceMeta | null;\n  currentRepository?: Repository;\n};\n\nconst ClipperHeader: React.FC<PageProps> = props => {\n  const {\n    form: { getFieldDecorator, validateFields, getFieldsValue, setFieldsValue },\n    form,\n    pathname,\n    service,\n    currentRepository,\n  } = props;\n  const formValue = getFieldsValue() as ClipperHeaderForm;\n  const ref = useRef<ClipperHeaderForm>(formValue);\n  const { loading, clipperHeaderForm } = useSelector((g: GlobalStore) => {\n    return {\n      loading: g.loading.effects[asyncCreateDocument.started.type],\n      clipperHeaderForm: g.clipper.clipperHeaderForm,\n    };\n  }, isEqual);\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    if (isEqual(clipperHeaderForm, ref.current)) {\n      return;\n    }\n    setFieldsValue(clipperHeaderForm);\n  }, [clipperHeaderForm, formValue, setFieldsValue]);\n\n  useEffect(() => {\n    if (isEqual(ref.current, formValue)) {\n      return;\n    }\n    dispatch(updateClipperHeader(formValue));\n    ref.current = formValue;\n  }, [dispatch, formValue]);\n\n  const handleSubmit = () => {\n    validateFields(err => {\n      if (err) {\n        return;\n      }\n      dispatch(asyncCreateDocument.started({ pathname }));\n    });\n  };\n\n  const headerForm = useMemo(() => {\n    const HeaderForm = service?.headerForm;\n    return HeaderForm ? <HeaderForm form={form} currentRepository={currentRepository} /> : null;\n  }, [currentRepository, form, service]);\n\n  return (\n    <Section\n      title={<FormattedMessage id=\"tool.title\" defaultMessage=\"Title\" />}\n      className={classNames(styles.header, styles.section)}\n    >\n      <Form.Item>\n        {getFieldDecorator('title', {\n          rules: [\n            {\n              required: true,\n              message: <FormattedMessage id=\"tool.title.required\" />,\n            },\n          ],\n        })(<Input placeholder=\"Please Input Title\" />)}\n      </Form.Item>\n      {headerForm}\n      <Button\n        className={styles.saveButton}\n        size=\"large\"\n        type=\"primary\"\n        title={\n          !currentRepository\n            ? localeService.format({\n                id: 'tool.saveButton.noRepository',\n              })\n            : ''\n        }\n        onClick={handleSubmit}\n        loading={loading}\n        disabled={loading || pathname === '/' || !currentRepository}\n        block\n      >\n        <FormattedMessage id=\"tool.save\" defaultMessage=\"Save Content\" />\n      </Button>\n    </Section>\n  );\n};\n\nexport default Form.create<PageProps>()(ClipperHeader);\n"
  },
  {
    "path": "src/pages/tool/index.less",
    "content": ".menuButton {\n  text-align: left;\n  border: none;\n  box-shadow: none;\n  span {\n    font-size: 16px;\n  }\n}\n\n.save-button {\n  span {\n    font-size: 16px;\n  }\n}\n.toolbar {\n  font-size: 18px;\n  display: flex;\n  justify-content: space-between;\n}\n.toolbarButton {\n  border: none;\n  columns: #e8e8e8;\n  font-size: 18px;\n  box-shadow: none;\n}\n.header {\n  :global(.ant-legacy-form-item) {\n    margin-bottom: 8px;\n  }\n}\n.section {\n  margin-bottom: 16px;\n}\n"
  },
  {
    "path": "src/pages/tool/index.tsx",
    "content": "import React, { useEffect, useMemo, useCallback } from 'react';\nimport styles from './index.less';\nimport ClipExtension from './ClipExtension';\nimport ToolExtensions from './toolExtensions';\nimport { CaretDownOutlined, SettingOutlined } from '@ant-design/icons';\nimport { Button, Badge, Dropdown, Menu } from 'antd';\nimport { connect, routerRedux } from 'dva';\nimport { GlobalStore } from '@/common/types';\nimport { isEqual } from 'lodash';\nimport { ToolContainer } from 'components/container';\nimport { selectRepository, asyncChangeAccount } from 'pageActions/clipper';\nimport { asyncRunExtension } from 'pageActions/userPreference';\nimport Section from 'components/section';\nimport { DvaRouterProps } from 'common/types';\nimport useFilterExtensions from '@/common/hooks/useFilterExtensions';\nimport { FormattedMessage } from 'react-intl';\nimport matchUrl from '@/common/matchUrl';\nimport Header from './Header';\nimport RepositorySelect from '@/components/RepositorySelect';\nimport Container from 'typedi';\nimport { IConfigService } from '@/service/common/config';\nimport { Observer, useObserver } from 'mobx-react';\nimport IconAvatar from '@/components/avatar';\nimport UserItem from '@/components/userItem';\nimport { IContentScriptService } from '@/service/common/contentScript';\nimport { IExtensionService, IExtensionContainer } from '@/service/common/extension';\nimport { IExtensionWithId, InitContext } from '@/extensions/common';\n\nconst mapStateToProps = ({\n  clipper: {\n    currentAccountId,\n    url,\n    currentRepository,\n    repositories,\n    currentImageHostingService,\n    clipperData,\n  },\n  loading,\n  account: { accounts },\n  userPreference: { locale, servicesMeta },\n}: GlobalStore) => {\n  const currentAccount = accounts.find(o => o.id === currentAccountId);\n  const loadingAccount = loading.effects[asyncChangeAccount.started.type];\n  return {\n    hasEditor: typeof clipperData['/editor'] !== 'undefined',\n    loadingAccount,\n    accounts,\n    currentImageHostingService,\n    url,\n    currentAccountId,\n    currentRepository,\n    currentAccount,\n    repositories,\n    locale,\n    servicesMeta,\n  };\n};\ntype PageStateProps = ReturnType<typeof mapStateToProps>;\ntype PageProps = PageStateProps & DvaRouterProps;\n\nconst Page = React.memo<PageProps>(\n  props => {\n    const extensionService = Container.get(IExtensionService);\n    const {\n      repositories,\n      currentAccount,\n      currentRepository,\n      loadingAccount,\n      url,\n      currentImageHostingService,\n      history: {\n        location: { pathname },\n      },\n      dispatch,\n      accounts,\n      servicesMeta,\n      hasEditor,\n    } = props;\n\n    const extensions = useObserver(() => {\n      return Container.get(IExtensionContainer)\n        .extensions.filter(o => !extensionService.DisabledExtensionIds.includes(o.id))\n        .filter(o => {\n          return !o.manifest.powerpack;\n        })\n        .filter(o => {\n          const matches = o.manifest.matches;\n          if (Array.isArray(matches)) {\n            // eslint-disable-next-line max-nested-callbacks\n            return matches.some(o => matchUrl(o, url!));\n          }\n          return true;\n        });\n    });\n\n    const configService = Container.get(IConfigService);\n\n    const currentService = currentAccount ? servicesMeta[currentAccount.type] : null;\n\n    useEffect(() => {\n      if (pathname === '/') {\n        if (accounts.length === 0) {\n          dispatch(routerRedux.push('/preference/account'));\n          return;\n        }\n      }\n    }, [accounts.length, dispatch, pathname]);\n\n    const onRepositorySelect = useCallback(\n      (repositoryId: string) => {\n        dispatch(selectRepository({ repositoryId }));\n      },\n      [dispatch]\n    );\n    let repositoryId: string | undefined;\n    if (currentRepository) {\n      repositoryId = currentRepository.id;\n    }\n    useEffect(() => {\n      if (currentAccount && currentAccount.defaultRepositoryId) {\n        if (repositoryId) {\n          return;\n        }\n        onRepositorySelect(currentAccount.defaultRepositoryId);\n      }\n    }, [repositoryId, currentAccount, onRepositorySelect]);\n\n    const push = (path: string) => dispatch(routerRedux.push(path));\n\n    const enableExtensions: IExtensionWithId[] = extensions.filter(o => {\n      if (o.extensionLifeCycle.init) {\n        const context: InitContext = {\n          locale: props.locale,\n          accountInfo: {\n            type: currentAccount && currentAccount.type,\n          },\n          url,\n          pathname,\n          currentImageHostingService,\n        };\n        return o.extensionLifeCycle.init(context);\n      }\n      return true;\n    });\n\n    const [toolExtensions, clipExtensions] = useFilterExtensions(enableExtensions);\n\n    const header = useMemo(() => {\n      return (\n        <Header\n          pathname={pathname}\n          service={currentService}\n          currentRepository={currentRepository}\n        />\n      );\n    }, [pathname, currentService, currentRepository]);\n\n    const overlay = useMemo(() => {\n      return (\n        <Menu onClick={e => dispatch(asyncChangeAccount.started({ id: e.key as string }))}>\n          {props.accounts.map(o => (\n            <Menu.Item key={o.id} title={o.name}>\n              <UserItem\n                avatar={o.avatar}\n                name={o.name}\n                description={o.description}\n                icon={servicesMeta[o.type].icon}\n              />\n            </Menu.Item>\n          ))}\n        </Menu>\n      );\n    }, [dispatch, props.accounts, servicesMeta]);\n\n    const dropdown = (\n      <Dropdown overlay={overlay} placement=\"bottomRight\">\n        <div style={{ display: 'flex', alignItems: 'center' }}>\n          {!!currentAccount && (\n            <IconAvatar\n              size=\"small\"\n              avatar={currentAccount.avatar}\n              icon={servicesMeta[currentAccount.type].icon}\n            />\n          )}\n          <CaretDownOutlined\n            style={{ fontSize: 8, color: 'rgb(140, 140, 140)', marginLeft: 6 }}\n          ></CaretDownOutlined>\n        </div>\n      </Dropdown>\n    );\n\n    return (\n      <ToolContainer onClickCloseButton={Container.get(IContentScriptService).hide}>\n        {header}\n        <ToolExtensions\n          extensions={toolExtensions}\n          onClick={extension =>\n            dispatch(\n              asyncRunExtension.started({\n                pathname,\n                extension,\n              })\n            )\n          }\n        />\n        <ClipExtension\n          hasEditor={hasEditor}\n          extensions={clipExtensions}\n          onClick={router => push(router)}\n          pathname={pathname}\n        />\n        <Section className={styles.section} title={<FormattedMessage id=\"tool.repository\" />}>\n          <RepositorySelect\n            disabled={loadingAccount}\n            loading={loadingAccount}\n            repositories={repositories}\n            onSelect={onRepositorySelect}\n            style={{ width: '100%' }}\n            dropdownMatchSelectWidth={true}\n            value={repositoryId}\n          />\n        </Section>\n        <Section>\n          <div className={styles.toolbar}>\n            <Button\n              className={styles.toolbarButton}\n              onClick={() => {\n                if (pathname.startsWith('/preference')) {\n                  push('/');\n                } else {\n                  push('/preference/account');\n                }\n              }}\n            >\n              <Observer>\n                {() => (\n                  <Badge dot={!configService.isLatestVersion}>\n                    <SettingOutlined style={{ fontSize: 18 }} />\n                  </Badge>\n                )}\n              </Observer>\n            </Button>\n            {dropdown}\n          </div>\n        </Section>\n      </ToolContainer>\n    );\n  },\n  (prevProps: PageProps, nextProps: PageProps) => {\n    const selector = ({\n      repositories,\n      currentAccount,\n      currentRepository,\n      history,\n      loadingAccount,\n      locale,\n      servicesMeta,\n      accounts,\n      hasEditor,\n    }: PageProps) => {\n      return {\n        hasEditor,\n        loadingAccount,\n        currentRepository,\n        repositories,\n        currentAccount,\n        pathname: history.location.pathname,\n        locale,\n        servicesMeta,\n        accounts,\n      };\n    };\n    return isEqual(selector(prevProps), selector(nextProps));\n  }\n);\n\nexport default connect(mapStateToProps)(Page as React.FC<PageProps>);\n"
  },
  {
    "path": "src/pages/tool/toolExtensions.tsx",
    "content": "import * as React from 'react';\nimport styles from './index.less';\nimport { Button } from 'antd';\nimport Section from 'components/section';\nimport { FormattedMessage } from 'react-intl';\nimport IconFont from '@/components/IconFont';\nimport { IExtensionWithId } from '@/extensions/common';\n\ntype ToolExtensionsProps = {\n  extensions: IExtensionWithId[];\n  onClick(router: IExtensionWithId): void;\n};\n\nconst ToolExtensions: React.FC<ToolExtensionsProps> = ({ extensions, onClick }) => {\n  if (extensions.length === 0) {\n    return null;\n  }\n  return (\n    <Section\n      className={styles.section}\n      title={\n        <FormattedMessage\n          id=\"tool.toolExtensions\"\n          defaultMessage=\"Tool Extensions\"\n        ></FormattedMessage>\n      }\n    >\n      {extensions.map(o => (\n        <Button\n          key={o.id}\n          className={styles.menuButton}\n          title={o.manifest.description}\n          onClick={() => onClick(o)}\n        >\n          <IconFont type={o.manifest.icon} />\n        </Button>\n      ))}\n    </Section>\n  );\n};\n\nexport default ToolExtensions;\n"
  },
  {
    "path": "src/service/common/config.ts",
    "content": "import { Token } from 'typedi';\nimport { ObservableSet } from 'mobx';\n\nexport interface RemoteConfig {\n  iconfont: string;\n\n  chromeWebStoreVersion: string;\n\n  privacyLocale: string[];\n\n  changelogLocale: string[];\n}\n\nexport interface IConfigService {\n  config?: RemoteConfig;\n\n  isLatestVersion: boolean;\n\n  readonly localVersion: string;\n\n  remoteIconSet: ObservableSet<string>;\n\n  id: string;\n\n  load(): Promise<void>;\n}\n\nexport const IConfigService = new Token<IConfigService>();\n"
  },
  {
    "path": "src/service/common/configuration.ts",
    "content": "export interface WebClipperConfiguration {\n  resource: {\n    host: string;\n    privacy: string;\n    changelog: string;\n  };\n  yuque_oauth: {\n    clientId: string;\n    callback: string;\n    scope: string;\n  };\n  onenote_oauth: {\n    clientId: string;\n    callback: string;\n  };\n  google_oauth: {\n    clientId: string;\n    callback: string;\n  };\n  github_oauth: {\n    clientId: string;\n    callback: string;\n  };\n}\n\nexport interface IConfigurationService {}\n\nexport type GetLocalConfiguration = () => {};\n"
  },
  {
    "path": "src/service/common/contentScript.ts",
    "content": "import { Token } from 'typedi';\n\nexport interface IToggleConfig {\n  pathname: string;\n  query?: string;\n}\nexport interface IContentScriptService {\n  hide(): Promise<void>;\n  remove(): Promise<void>;\n  checkStatus(): Promise<boolean>;\n  toggle(config?: IToggleConfig): Promise<void>;\n  runScript(id: string, lifeCycle: 'run' | 'destroy'): Promise<void>;\n  getSelectionMarkdown(): Promise<string>;\n  getPageUrl(): Promise<string>;\n}\n\nexport const IContentScriptService = new Token<IContentScriptService>('IContentScriptService');\n"
  },
  {
    "path": "src/service/common/cookie.ts",
    "content": "import { Token } from 'typedi';\n\nexport interface ICookieService {\n  get(details: chrome.cookies.Details): Promise<chrome.cookies.Cookie | null>;\n  getAll(details: chrome.cookies.GetAllDetails): Promise<chrome.cookies.Cookie[]>;\n  getAllCookieStores(): Promise<chrome.cookies.CookieStore[]>;\n}\n\nexport const ICookieService = new Token<ICookieService>('ICookieService');\n"
  },
  {
    "path": "src/service/common/extension.ts",
    "content": "import { IExtensionWithId, IContextMenusWithId } from '@/extensions/common';\nimport { Token } from 'typedi';\n\nexport interface Extension {\n  //\n}\n\nexport interface IExtensionService {\n  init(): Promise<void>;\n  DefaultExtensionId: string | null;\n\n  DisabledExtensionIds: string[];\n\n  EnabledAutomaticExtensionIds: string[];\n\n  getExtensionConfig<T>(id: string): T | undefined;\n\n  setExtensionConfig<T = any>(id: string, data: T): Promise<void>;\n\n  toggleDefault(id: string): Promise<void>;\n\n  toggleDisableExtension(id: string): Promise<void>;\n\n  toggleAutomaticExtension(id: string): Promise<void>;\n}\n\nexport interface IExtensionContainer {\n  init(): Promise<void>;\n  extensions: IExtensionWithId[];\n  contextMenus: IContextMenusWithId[];\n}\n\nexport const IExtensionService = new Token<IExtensionService>();\n\nexport const IExtensionContainer = new Token<IExtensionContainer>();\n"
  },
  {
    "path": "src/service/common/ipc.ts",
    "content": "import { generateUuid } from '@web-clipper/shared/lib/uuid';\nimport { SerializedError } from '@/common/error';\n\nexport interface IServerChannel<C = any> {\n  callCommand<T = any>(context: C, command: string, arg?: any): Promise<T>;\n}\n\nexport interface IChannel {\n  call<T>(command: string, arg?: any): Promise<T>;\n}\n\nexport interface IChannelClient {\n  getChannel(channelName: string): IChannel;\n}\n\nexport interface IChannelServer {\n  registerChannel(channelName: string, channel: IServerChannel): void;\n}\n\nexport interface IPCMessageRequest<T = any> {\n  uuid: string;\n  command: string;\n  arg?: T;\n}\n\nexport interface IPCMessageResponse<T = any> {\n  uuid: string;\n  result?: {\n    data: T;\n  };\n  error?: {\n    data: SerializedError;\n  };\n}\n\nexport class ChannelClient implements IChannel {\n  constructor(private channelName: string) {}\n\n  async call<T>(command: string, arg?: any): Promise<T> {\n    const uuid = generateUuid();\n    const response: any = await chrome.runtime.sendMessage({\n      uuid,\n      command: command,\n      arg,\n      channelName: this.channelName,\n    });\n    if (response.error) {\n      const errorData: SerializedError = response.error.data;\n      if (errorData.$isError) {\n        const error = new Error(errorData.message);\n        error.name = errorData.name;\n        error.stack = errorData.stack;\n        throw error;\n      } else {\n        throw response.error.data;\n      }\n    }\n    if (response.result) {\n      return response.result.data;\n    }\n    throw new Error('some error');\n  }\n}\n"
  },
  {
    "path": "src/service/common/locale.ts",
    "content": "import { Token } from 'typedi';\nimport { MessageDescriptor } from 'react-intl';\n\nexport interface ILocaleService {\n  locale: string;\n  init(): Promise<void>;\n  format(descriptor: MessageDescriptor): string;\n}\n\nexport const ILocaleService = new Token<ILocaleService>('locale');\n"
  },
  {
    "path": "src/service/common/permissions.ts",
    "content": "import { Token } from 'typedi';\n\nexport interface Permissions {\n  origins?: string[];\n\n  permissions?: string[];\n}\n\nexport interface IPermissionsService {\n  contains(permissions: Permissions): Promise<boolean>;\n\n  request(permissions: Permissions): Promise<boolean>;\n\n  remove(permissions: Permissions): Promise<boolean>;\n}\n\nexport const IPermissionsService = new Token<IPermissionsService>('IPermissionsService');\n"
  },
  {
    "path": "src/service/common/preference.ts",
    "content": "import { Token } from 'typedi';\n\nexport type TIconColor = 'dark' | 'light' | 'auto';\n\nexport interface IUserPreference {\n  iconColor: TIconColor;\n}\n\nexport interface IPreferenceService {\n  userPreference: IUserPreference;\n  init: () => Promise<void>;\n\n  updateIconColor(color: TIconColor): Promise<void>;\n}\n\nexport const IPreferenceService = new Token<IPreferenceService>();\n"
  },
  {
    "path": "src/service/common/request.ts",
    "content": "import { Token } from 'typedi';\n\nexport type Method = 'get' | 'post' | 'put';\n\nexport interface BaseRequestOptions {\n  method: Method;\n  headers?: Record<string, string>;\n}\n\nexport interface IPostFormRequestOptions extends BaseRequestOptions {\n  method: 'post';\n  requestType: 'form';\n  data: FormData;\n}\nexport interface IPostRequestOptions extends BaseRequestOptions {\n  method: 'post';\n  requestType: 'json';\n  data: any;\n}\n\nexport interface IGetFormRequestOptions extends BaseRequestOptions {\n  method: 'get';\n}\n\nexport interface IPutRequestOptions extends BaseRequestOptions {\n  method: 'put';\n  data: any;\n}\n\nexport type TRequestOption =\n  | IGetFormRequestOptions\n  | IPostRequestOptions\n  | IPostFormRequestOptions\n  | IPutRequestOptions;\n\nexport interface IRequestService {\n  request<T>(url: string, options: TRequestOption): Promise<T>;\n  download(url: string): Promise<Blob>;\n}\n\nexport type RequestInterceptor = (\n  url: string,\n  options: TRequestOption\n) => {\n  url?: string;\n  options?: TRequestOption;\n};\n\nexport type ResponseInterceptor = (data: unknown) => unknown;\n\nexport interface IExtendRequestHelper {\n  post<T>(url: string, options: Omit<IPostRequestOptions, 'method' | 'requestType'>): Promise<T>;\n  postForm<T>(\n    url: string,\n    options: Omit<IPostFormRequestOptions, 'method' | 'requestType'>\n  ): Promise<T>;\n\n  put<T>(url: string, options: Omit<IPutRequestOptions, 'method'>): Promise<T>;\n\n  get<T>(url: string, options?: Omit<IGetFormRequestOptions, 'method'>): Promise<T>;\n}\n\nexport interface IHelperOptions {\n  baseURL?: string;\n  headers?: Record<string, string>;\n  request: IRequestService;\n  params?: Record<string, string>;\n  interceptors?: {\n    request?: RequestInterceptor[] | RequestInterceptor;\n    response?: ResponseInterceptor[] | ResponseInterceptor;\n  };\n}\n\nexport const IBasicRequestService = new Token<IRequestService>();\n"
  },
  {
    "path": "src/service/common/storage.ts",
    "content": "import { Token } from 'typedi';\nimport { IStorageService } from '@web-clipper/shared/lib/storage';\n\nexport const ILocalStorageService = new Token<IStorageService>();\n\nexport const ISyncStorageService = new Token<IStorageService>();\n"
  },
  {
    "path": "src/service/common/tab.ts",
    "content": "import { Token } from 'typedi';\nexport interface Tab {\n  id?: number;\n  title?: string;\n  url?: string;\n}\n\nexport interface CaptureVisibleTabOptions {\n  quality?: number;\n\n  format?: string;\n}\nexport interface ITabService {\n  getCurrent(): Promise<Tab>;\n\n  closeCurrent(): Promise<void>;\n\n  remove(tabId: number): Promise<void>;\n\n  captureVisibleTab(option: CaptureVisibleTabOptions | number): Promise<string>;\n\n  sendMessage<T>(tabId: number, message: any): Promise<T>;\n\n  sendActionToCurrentTab<T>(action: any): Promise<T>;\n\n  create(createProperties: chrome.tabs.CreateProperties): Promise<chrome.tabs.Tab>;\n}\n\nexport abstract class AbstractTabService implements ITabService {\n  closeCurrent = async () => {\n    const current = await this.getCurrent();\n    return this.remove(current.id!);\n  };\n\n  sendActionToCurrentTab = async <T>(action: any): Promise<T> => {\n    const current = await this.getCurrent();\n    if (!current || !current.id) {\n      throw new Error('No Tab');\n    }\n    return this.sendMessage(current.id, action);\n  };\n\n  abstract getCurrent(): Promise<Tab>;\n  abstract remove(tabId: number): Promise<void>;\n  abstract captureVisibleTab(option: CaptureVisibleTabOptions | number): Promise<string>;\n  abstract sendMessage<T>(tabId: number, message: any): Promise<T>;\n  abstract create(createProperties: chrome.tabs.CreateProperties): Promise<chrome.tabs.Tab>;\n}\n\nexport const ITabService = new Token<ITabService>('ITabService');\n"
  },
  {
    "path": "src/service/common/webRequest.ts",
    "content": "import { Token } from 'typedi';\n\nexport interface WebBlockHeader {\n  name: string;\n  value: string;\n}\n\nexport interface WebRequestBlockOption {\n  requestHeaders: chrome.webRequest.HttpHeader[];\n  urls: string[];\n}\n\nexport interface RequestInBackgroundOptions {\n  method?: string;\n  data?: any;\n  prefix?: string;\n  headers?: HeadersInit;\n}\n\nexport interface IWebRequestService {\n  startChangeHeader(option: WebRequestBlockOption): Promise<WebBlockHeader>;\n\n  end(webBlockHeader: WebBlockHeader): Promise<void>;\n\n  requestInBackground<T>(url: string, options?: RequestInBackgroundOptions): Promise<T>;\n\n  changeUrl(url: string, query: WebBlockHeader): Promise<string>;\n}\n\nexport const IWebRequestService = new Token<IWebRequestService>('IWebRequestService');\n"
  },
  {
    "path": "src/service/config/browser/configService.ts",
    "content": "import { hasUpdate } from '@/common/version';\nimport { RemoteConfig as _RemoteConfig, IConfigService } from '@/service/common/config';\nimport { Service } from 'typedi';\nimport packageJson from '@/../package.json';\nimport localConfig from '@/../config.json';\nimport { observable, ObservableSet, runInAction } from 'mobx';\nimport request from 'umi-request';\nimport { getResourcePath } from '@/common/getResource';\n\ntype RemoteConfig = _RemoteConfig;\n\nclass BrowserConfigService implements IConfigService {\n  @observable\n  public isLatestVersion: boolean = true;\n\n  @observable\n  public config: RemoteConfig = localConfig;\n\n  @observable\n  public remoteIconSet: ObservableSet<string> = observable.set<string>();\n\n  public readonly localVersion = packageJson.version;\n\n  load = async () => {\n    const iconsFile = await request.get('./icon.js');\n    const matchResult: string[] = iconsFile.match(/id=\"([A-Za-z]+)\"/g) || [];\n    const remoteIcons = matchResult.map((o) => o.match(/id=\"([A-Za-z]+)\"/)![1]);\n    runInAction(() => {\n      remoteIcons.forEach((icon) => {\n        this.remoteIconSet.add(icon);\n      });\n    });\n    try {\n      runInAction(() => {\n        this.isLatestVersion = !hasUpdate(this.config.chromeWebStoreVersion, this.localVersion);\n      });\n    } catch (_error) {\n      console.log('Load Config Error');\n    }\n  };\n\n  get id() {\n    const url = chrome.runtime.getURL('tool.html');\n    const match = /(chrome-extension|moz-extension):\\/\\/(.*)\\/tool.html/.exec(url);\n    if (!match) {\n      throw new Error('Get ExtensionId failed');\n    }\n    return match[2];\n  }\n}\n\nService(IConfigService)(BrowserConfigService);\n"
  },
  {
    "path": "src/service/configuration/common/generate-local-config.ts",
    "content": "import { WebClipperConfiguration } from '@/service/common/configuration';\n\ninterface IGenerateLocalConfigOptions {\n  locale: string;\n}\n\nconst generateLocalConfig = (_options: IGenerateLocalConfigOptions): WebClipperConfiguration => {\n  return {\n    resource: {\n      host: '',\n      privacy: '',\n      changelog: '',\n    },\n    yuque_oauth: {\n      clientId: 'D1AwzCeDPLFWGfcGv7ze',\n      callback: 'http://webclipper-oauth.yfd.im/yuque_oauth',\n      scope: '94f779401c7bed8734ce',\n    },\n    onenote_oauth: {\n      clientId: '',\n      callback: '',\n    },\n    google_oauth: {\n      clientId: '',\n      callback: '',\n    },\n    github_oauth: {\n      clientId: '',\n      callback: '',\n    },\n  };\n};\n\nexport { generateLocalConfig };\n"
  },
  {
    "path": "src/service/configuration/configuration.ts",
    "content": "import { IStorageService } from '@web-clipper/shared/lib/storage';\nimport { Inject } from 'typedi';\nimport { ILocalStorageService } from './../common/storage';\nimport { IConfigurationService } from '@/service/common/configuration';\n\nexport class ConfigurationService implements IConfigurationService {\n  private _initialized: Promise<void> | null = null;\n  constructor(@Inject(ILocalStorageService) private localStorageService: IStorageService) {\n    //\n  }\n\n  public async init(): Promise<void> {\n    if (!this._initialized) {\n      this._initialized = this.doInitialize();\n    }\n    return this._initialized;\n  }\n\n  private async doInitialize(): Promise<void> {\n    await this.localStorageService.init();\n  }\n}\n"
  },
  {
    "path": "src/service/contentScript/browser/contentScript/contentScript.less",
    "content": ".toolFrame {\n  position: fixed !important;\n  right: 0 !important;\n  top: 0 !important;\n  width: 100% !important;\n  height: 100% !important;\n  z-index: 2147483646 !important;\n  border: none !important;\n}\n\n.web-clipper-loading-box {\n  position: fixed;\n  right: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 2147483646;\n  border: none;\n  :global {\n    .web-clipper-loading {\n      position: fixed;\n      right: 10px;\n      top: 10px;\n      box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px;\n      background: white;\n      width: 324px;\n      height: 150px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n    }\n\n    .web-clipper-loading .line :local {\n      animation: expand 1s ease-in-out infinite;\n      border-radius: 10px;\n      display: inline-block;\n      transform-origin: center center;\n      margin: 0 3px;\n      width: 1px;\n      height: 25px;\n    }\n\n    .web-clipper-loading .line:nth-child(1) {\n      background: #27ae60;\n    }\n\n    .web-clipper-loading .line:nth-child(2) {\n      animation-delay: 180ms;\n      background: #f1c40f;\n    }\n\n    .web-clipper-loading .line:nth-child(3) {\n      animation-delay: 360ms;\n      background: #e67e22;\n    }\n\n    .web-clipper-loading .line:nth-child(4) {\n      animation-delay: 540ms;\n      background: #2980b9;\n    }\n\n    @keyframes expand {\n      0% {\n        transform: scale(1);\n      }\n      25% {\n        transform: scale(2);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/service/contentScript/browser/contentScript/contentScript.ts",
    "content": "import { IContentScriptService, IToggleConfig } from '@/service/common/contentScript';\nimport { Service, Inject } from 'typedi';\nimport styles from '@/service/contentScript/browser/contentScript/contentScript.less';\nimport * as QRCode from 'qrcode';\nimport { Readability } from '@web-clipper/readability';\nimport AreaSelector from '@web-clipper/area-selector';\nimport Highlighter from '@web-clipper/highlight';\nimport plugins from '@web-clipper/turndown';\nimport TurndownService from 'turndown';\nimport { ContentScriptContext } from '@/extensions/common';\nimport { localStorageService } from '@/common/chrome/storage';\nimport { LOCAL_USER_PREFERENCE_LOCALE_KEY } from '@/common/types';\nimport { IExtensionContainer } from '@/service/common/extension';\nimport { getResourcePath } from '@/common/getResource';\n\nconst turndownService = new TurndownService({ codeBlockStyle: 'fenced' });\nturndownService.use(plugins);\nclass ContentScriptService implements IContentScriptService {\n  constructor(@Inject(IExtensionContainer) private extensionContainer: IExtensionContainer) {}\n\n  async remove() {\n    $(`.${styles.toolFrame}`).remove();\n  }\n  async hide() {\n    $(`.${styles.toolFrame}`).hide();\n  }\n  async toggle(config: IToggleConfig) {\n    const toolPath = getResourcePath('tool.html');\n    let src = chrome.runtime.getURL(toolPath);\n    if (config) {\n      src = `${chrome.runtime.getURL(toolPath)}#${config.pathname}?${config.query}`;\n    }\n    if ($(`.${styles.toolFrame}`).length === 0) {\n      if (config) {\n        $('body').append(`<iframe src=\"${src}\" class=${styles.toolFrame}></iframe>`);\n        return;\n      }\n      $('body').append(`<iframe src=\"${src}\" class=${styles.toolFrame}></iframe>`);\n    } else {\n      const srcRaw = $(`.${styles.toolFrame}`).attr('src');\n\n      if (srcRaw !== src) {\n        $(`.${styles.toolFrame}`).attr('src', src);\n      }\n      $(`.${styles.toolFrame}`).toggle();\n    }\n  }\n  async getSelectionMarkdown() {\n    let selection = document.getSelection();\n    if (selection?.rangeCount) {\n      let container = document.createElement('div');\n      for (let i = 0, len = selection.rangeCount; i < len; ++i) {\n        container.appendChild(selection.getRangeAt(i).cloneContents());\n      }\n      return turndownService.turndown(container.innerHTML);\n    }\n    return '';\n  }\n  async checkStatus() {\n    return true;\n  }\n  async getPageUrl() {\n    return location.href;\n  }\n  async toggleLoading() {\n    const loadIngStyle = styles['web-clipper-loading-box'];\n    if ($(`.${loadIngStyle}`).length === 0) {\n      $('body').append(`\n      <div class=${loadIngStyle}>\n        <div class=\"web-clipper-loading\">\n          <div>\n            <div class=\"line\"></div>\n            <div class=\"line\"></div>\n            <div class=\"line\"></div>\n            <div class=\"line\"></div>\n          </div>\n        </div>\n      </div>\n      `);\n    } else {\n      $(`.${loadIngStyle}`).remove();\n    }\n  }\n\n  async runScript(id: string, lifeCycle: 'run' | 'destroy') {\n    const extensions = this.extensionContainer.extensions;\n    const extension = extensions.find((o) => o.id === id);\n    const lifeCycleFunc = extension?.extensionLifeCycle[lifeCycle];\n    if (!lifeCycleFunc) {\n      return;\n    }\n    await localStorageService.init();\n    const toggleClipper = () => {\n      $(`.${styles.toolFrame}`).toggle();\n    };\n    const context: ContentScriptContext = {\n      locale: localStorageService.get(LOCAL_USER_PREFERENCE_LOCALE_KEY, navigator.language),\n      turndown: turndownService,\n      Highlighter: Highlighter,\n      toggleClipper,\n      Readability,\n      document,\n      AreaSelector,\n      QRCode,\n      $,\n      toggleLoading: () => {\n        this.toggleLoading();\n      },\n    };\n    $(`.${styles.toolFrame}`).blur();\n    return lifeCycleFunc(context);\n  }\n}\n\nService(IContentScriptService)(ContentScriptService);\n"
  },
  {
    "path": "src/service/contentScript/common/contentScriptIPC.ts",
    "content": "import { IContentScriptService, IToggleConfig } from '@/service/common/contentScript';\nimport { IServerChannel, IChannel } from '@/service/common/ipc';\n\nexport class ContentScriptChannel implements IServerChannel {\n  constructor(private service: IContentScriptService) {}\n\n  callCommand = async (\n    _context: chrome.runtime.Port['sender'],\n    command: string,\n    arg: any\n  ): Promise<any> => {\n    switch (command) {\n      case 'remove':\n        return this.service.remove();\n      case 'hide':\n        return this.service.hide();\n      case 'checkStatus':\n        return this.service.checkStatus();\n      case 'getPageUrl':\n        return this.service.getPageUrl();\n      case 'toggle': {\n        return this.service.toggle(arg[0]);\n      }\n      case 'getSelectionMarkdown':\n        return this.service.getSelectionMarkdown();\n      case 'runScript': {\n        return this.service.runScript(arg[0], arg[1]);\n      }\n      default: {\n        throw new Error(`Call not found: ${command}`);\n      }\n    }\n  };\n}\n\nexport class ContentScriptChannelClient implements IContentScriptService {\n  constructor(private channel: IChannel) {}\n\n  remove = async (): Promise<void> => {\n    return this.channel.call('remove');\n  };\n\n  runScript = async (id: string, lifeCycle: 'run' | 'destroy'): Promise<void> => {\n    return this.channel.call('runScript', [id, lifeCycle]);\n  };\n\n  hide = async (): Promise<void> => {\n    return this.channel.call('hide');\n  };\n  checkStatus = async (): Promise<boolean> => {\n    return this.channel.call('checkStatus');\n  };\n  toggle = async (config?: IToggleConfig): Promise<void> => {\n    return this.channel.call('toggle', [config]);\n  };\n  getSelectionMarkdown = async (): Promise<string> => {\n    return this.channel.call('getSelectionMarkdown');\n  };\n  getPageUrl = async (): Promise<string> => {\n    return this.channel.call('getPageUrl');\n  };\n}\n"
  },
  {
    "path": "src/service/cookie/background/cookieService.ts",
    "content": "import { Service } from 'typedi';\nimport { ICookieService } from '@/service/common/cookie';\n\nclass ChromeCookieService implements ICookieService {\n  get(detail: chrome.cookies.Details): Promise<chrome.cookies.Cookie | null> {\n    return new Promise<chrome.cookies.Cookie | null>(r => {\n      chrome.cookies.get(detail, r);\n    });\n  }\n\n  getAll(detail: chrome.cookies.GetAllDetails): Promise<chrome.cookies.Cookie[]> {\n    return new Promise<chrome.cookies.Cookie[]>(r => {\n      chrome.cookies.getAll(detail, r);\n    });\n  }\n  getAllCookieStores(): Promise<chrome.cookies.CookieStore[]> {\n    return new Promise<chrome.cookies.CookieStore[]>(r => {\n      chrome.cookies.getAllCookieStores(cookieStores => {\n        r(cookieStores);\n      });\n    });\n  }\n}\n\nService(ICookieService)(ChromeCookieService);\n"
  },
  {
    "path": "src/service/cookie/common/cookieIpc.ts",
    "content": "import { IServerChannel, IChannel } from '@/service/common/ipc';\nimport { ICookieService } from '@/service/common/cookie';\n\nexport class CookieChannel implements IServerChannel {\n  constructor(private service: ICookieService) {}\n\n  callCommand = async (\n    _context: chrome.runtime.Port['sender'],\n    command: string,\n    arg: any\n  ): Promise<any> => {\n    switch (command) {\n      case 'get':\n        return this.service.get(arg);\n      case 'getAll':\n        return this.service.getAll(arg);\n      case 'getAllCookieStores':\n        return this.service.getAllCookieStores();\n      default: {\n        throw new Error(`Call not found: ${command}`);\n      }\n    }\n  };\n}\n\nexport class CookieChannelClient implements ICookieService {\n  constructor(private channel: IChannel) {}\n\n  get = async (detail: chrome.cookies.Details): Promise<chrome.cookies.Cookie | null> => {\n    return this.channel.call('get', detail);\n  };\n\n  getAll = async (detail: chrome.cookies.GetAllDetails): Promise<chrome.cookies.Cookie[]> => {\n    return this.channel.call('getAll', detail);\n  };\n\n  getAllCookieStores = async (): Promise<chrome.cookies.CookieStore[]> => {\n    return this.channel.call('getAllCookieStores');\n  };\n}\n"
  },
  {
    "path": "src/service/extension/browser/extensionContainer.ts",
    "content": "import { ILocaleService } from './../../common/locale';\nimport { LOCAL_USER_PREFERENCE_LOCALE_KEY } from '@/common/types';\nimport { getLocaleExtensionManifest, IExtensionWithId } from '@/extensions/common';\nimport { extensions, contextMenus } from '@/extensions';\nimport { IStorageService } from '@web-clipper/shared/lib/storage';\nimport { ILocalStorageService } from '@/service/common/storage';\nimport { Service, Inject } from 'typedi';\nimport { IExtensionContainer } from '@/service/common/extension';\nimport { observable } from 'mobx';\nimport { IContextMenusWithId } from '@/extensions/contextMenus';\n\nclass ExtensionContainer implements IExtensionContainer {\n  @observable\n  public extensions: IExtensionWithId[] = [];\n\n  @observable\n  public contextMenus: IContextMenusWithId[] = [];\n\n  constructor(\n    @Inject(ILocalStorageService) private localStorageService: IStorageService,\n    @Inject(ILocaleService) private localeService: ILocaleService\n  ) {\n    this.localeService.init().then(() => {\n      this.init();\n    });\n    this.localStorageService.onDidChangeStorage((e) => {\n      if (e === LOCAL_USER_PREFERENCE_LOCALE_KEY) {\n        this.init();\n      }\n    });\n  }\n\n  async init() {\n    await this.localeService.init();\n    await this.localeService.init();\n    const locale = this.localStorageService.get(\n      LOCAL_USER_PREFERENCE_LOCALE_KEY,\n      navigator.language\n    );\n    const internalExtensions = extensions.map((e) => {\n      let extensionInstance: any = e;\n      if (e.factory) {\n        const Factory = e.factory;\n        extensionInstance = { ...e, ...new Factory() };\n      }\n      return {\n        ...extensionInstance,\n        manifest: getLocaleExtensionManifest(extensionInstance.manifest, locale),\n      };\n    });\n    this.extensions = internalExtensions;\n    this.contextMenus = contextMenus;\n  }\n}\n\nService(IExtensionContainer)(ExtensionContainer);\n"
  },
  {
    "path": "src/service/extension/browser/extensionService.ts",
    "content": "import {\n  LOCAL_EXTENSIONS_ENABLE_AUTOMATIC_EXTENSIONS_KEY,\n  LOCAL_EXTENSIONS_DISABLED_EXTENSIONS_KEY,\n} from '@/common/modelTypes/extensions';\nimport { IStorageService } from '@web-clipper/shared/lib/storage';\nimport { ILocalStorageService, ISyncStorageService } from '@/service/common/storage';\nimport { Service, Inject } from 'typedi';\nimport { IExtensionService } from '@/service/common/extension';\nimport { observable } from 'mobx';\n\nclass ExtensionService implements IExtensionService {\n  @observable\n  public DefaultExtensionId: string | null = null;\n\n  @observable\n  public DisabledExtensionIds: string[] = [];\n\n  @observable\n  public EnabledAutomaticExtensionIds: string[] = [];\n\n  constructor(\n    @Inject(ILocalStorageService) private localStorageService: IStorageService,\n    @Inject(ISyncStorageService) private syncStorageService: IStorageService\n  ) {\n    this.init();\n    this.syncStorageService.onDidChangeStorage((e) => {\n      if (['defaultPluginId'].includes(e)) {\n        this.init();\n      }\n    });\n    this.localStorageService.onDidChangeStorage((e) => {\n      if (\n        [\n          LOCAL_EXTENSIONS_ENABLE_AUTOMATIC_EXTENSIONS_KEY,\n          LOCAL_EXTENSIONS_DISABLED_EXTENSIONS_KEY,\n        ].includes(e)\n      ) {\n        this.init();\n      }\n    });\n  }\n\n  getExtensionConfig<T>(id: string): T | undefined {\n    const config = this.localStorageService.get('extensionConfig', '{}');\n    if (JSON.parse(config)[id]) {\n      return JSON.parse(JSON.parse(config)[id]);\n    }\n    return;\n  }\n\n  async setExtensionConfig(id: string, data: any): Promise<void> {\n    const config = JSON.parse(this.localStorageService.get('extensionConfig', '{}'));\n    config[id] = JSON.stringify(data);\n    await this.localStorageService.set('extensionConfig', JSON.stringify(config));\n  }\n\n  async toggleDefault(id: string) {\n    if (this.DefaultExtensionId === id) {\n      await this.syncStorageService.delete('defaultPluginId');\n      return;\n    }\n    await this.syncStorageService.set('defaultPluginId', id);\n  }\n\n  async toggleDisableExtension(id: string) {\n    await this.toggleStorageData(LOCAL_EXTENSIONS_DISABLED_EXTENSIONS_KEY, id);\n  }\n\n  async toggleAutomaticExtension(id: string) {\n    await this.toggleStorageData(LOCAL_EXTENSIONS_ENABLE_AUTOMATIC_EXTENSIONS_KEY, id);\n  }\n\n  private async toggleStorageData(key: string, id: string) {\n    let extensions = JSON.parse(this.localStorageService.get(key, '[]')) as string[];\n    const newExtensions = extensions.filter((o) => o !== id);\n    if (newExtensions.length === extensions.length) {\n      newExtensions.push(id);\n    }\n    await this.localStorageService.set(key, JSON.stringify(newExtensions));\n  }\n\n  async init() {\n    const DefaultExtensionId = this.syncStorageService.get('defaultPluginId');\n    this.DefaultExtensionId = DefaultExtensionId ?? null;\n\n    this.DisabledExtensionIds = JSON.parse(\n      this.localStorageService.get(LOCAL_EXTENSIONS_DISABLED_EXTENSIONS_KEY, '[]')\n    ) as string[];\n\n    this.DisabledExtensionIds = JSON.parse(\n      this.localStorageService.get(LOCAL_EXTENSIONS_DISABLED_EXTENSIONS_KEY, '[]')\n    ) as string[];\n\n    this.EnabledAutomaticExtensionIds = JSON.parse(\n      this.localStorageService.get(LOCAL_EXTENSIONS_ENABLE_AUTOMATIC_EXTENSIONS_KEY, '[]')\n    ) as string[];\n  }\n}\n\nService(IExtensionService)(ExtensionService);\n"
  },
  {
    "path": "src/service/ipc/browser/background-main/ipcService.ts",
    "content": "import { transformErrorForSerialization } from '@/common/error';\nimport { IChannelServer, IServerChannel } from '@/service/common/ipc';\n\nexport class BackgroundIPCServer implements IChannelServer {\n  public registerChannel(channelName: string, server: IServerChannel) {\n    chrome.runtime.onMessage.addListener((message: any, _sender, sendResponse) => {\n      if (channelName !== message.channelName) {\n        return false;\n      }\n      const { uuid, command, arg } = message;\n      server\n        .callCommand(_sender, command, arg)\n        .then((result) => {\n          sendResponse({\n            uuid,\n            result: { data: result },\n          });\n        })\n        .catch((error) => {\n          sendResponse({\n            uuid,\n            error: { data: transformErrorForSerialization(error) },\n          });\n        });\n      return true;\n    });\n  }\n}\n"
  },
  {
    "path": "src/service/ipc/browser/contentScript/contentScriptIPCServer.ts",
    "content": "import {\n  IChannelServer,\n  IServerChannel,\n  IPCMessageRequest,\n  IPCMessageResponse,\n} from '@/service/common/ipc';\nimport { transformErrorForSerialization } from '@/common/error';\n\nexport class ContentScriptIPCServer implements IChannelServer {\n  public registerChannel(\n    channelName: string,\n    server: IServerChannel<chrome.runtime.MessageSender>\n  ) {\n    const uuid = channelName;\n    chrome.runtime.onMessage.addListener(\n      (message: IPCMessageRequest, sender: chrome.runtime.MessageSender, sendResponse) => {\n        if (message.uuid !== uuid) {\n          return;\n        }\n        (async () => {\n          let response: IPCMessageResponse;\n          try {\n            const result = await server.callCommand(sender, message.command, message.arg);\n            response = {\n              uuid,\n              result: { data: result },\n            };\n          } catch (error) {\n            response = {\n              uuid,\n              error: { data: transformErrorForSerialization(error) },\n            };\n          }\n          sendResponse(response);\n        })();\n        return true;\n      }\n    );\n  }\n}\n"
  },
  {
    "path": "src/service/ipc/browser/popup/ipcClient.ts",
    "content": "import { SerializedError } from '@/common/error';\nimport { ITabService } from '@/service/common/tab';\nimport {\n  IChannelClient,\n  ChannelClient,\n  IChannel,\n  IPCMessageRequest,\n  IPCMessageResponse,\n} from '@/service/common/ipc';\n\nexport class PopupIpcClient implements IChannelClient {\n  getChannel(channelName: string) {\n    return new ChannelClient(channelName);\n  }\n}\n\nexport class PopupContentScriptChannelClient implements IChannel {\n  constructor(private namespace: string, private tabService: ITabService) {}\n\n  async call<T>(command: string, arg?: any): Promise<T> {\n    const action: IPCMessageRequest = {\n      uuid: this.namespace,\n      command,\n      arg,\n    };\n    const message: IPCMessageResponse<T> = await this.tabService.sendActionToCurrentTab(action);\n    if (!message) {\n      return Promise.reject(\n        new Error(chrome.runtime.lastError?.message ?? 'ContentScript not ready yet.')\n      );\n    }\n    return new Promise((resolve, reject) => {\n      if (message.error) {\n        const errorData: SerializedError = message.error.data;\n        if (errorData.$isError) {\n          const error = new Error(errorData.message);\n          error.name = errorData.name;\n          error.stack = errorData.stack;\n          reject(error);\n        } else {\n          reject(message.error.data);\n        }\n        return;\n      }\n      if (message.result) {\n        resolve(message.result.data);\n        return;\n      }\n    });\n  }\n}\n\nexport class PopupContentScriptIPCClient implements IChannelClient {\n  constructor(private tabService: ITabService) {}\n  getChannel(channelName: string) {\n    return new PopupContentScriptChannelClient(channelName, this.tabService);\n  }\n}\n"
  },
  {
    "path": "src/service/permissions/chrome/permissionsService.ts",
    "content": "import { IPermissionsService, Permissions } from '@/service/common/permissions';\nimport { Service } from 'typedi';\n\nclass PermissionsService implements IPermissionsService {\n  contains(p: Permissions) {\n    return new Promise<boolean>((r) => {\n      if (!chrome.permissions) {\n        r(true);\n        return;\n      }\n      chrome.permissions.contains(p, r);\n    });\n  }\n\n  remove(p: Permissions) {\n    return new Promise<boolean>((r) => {\n      if (!chrome.permissions) {\n        r(true);\n        return;\n      }\n      chrome.permissions.remove(p, r);\n    });\n  }\n\n  request(p: Permissions) {\n    return new Promise<boolean>((r) => {\n      if (!chrome.permissions) {\n        r(true);\n        return;\n      }\n      console.log('chrome.permissions.request ', chrome);\n      chrome.permissions.request(p, r);\n    });\n  }\n}\n\nService(IPermissionsService)(PermissionsService);\n"
  },
  {
    "path": "src/service/permissions/common/permissionsIpc.ts",
    "content": "import { IPermissionsService, Permissions } from '@/service/common/permissions';\nimport { IServerChannel, IChannel } from '@/service/common/ipc';\n\nexport class PermissionsChannel implements IServerChannel {\n  constructor(private service: IPermissionsService) {}\n\n  callCommand = async (\n    _context: chrome.runtime.Port['sender'],\n    command: string,\n    arg: any\n  ): Promise<any> => {\n    switch (command) {\n      case 'contains':\n        return this.service.contains(arg);\n      case 'remove':\n        return this.service.remove(arg);\n      case 'request':\n        return this.service.request(arg);\n      default: {\n        throw new Error(`Call not found: ${command}`);\n      }\n    }\n  };\n}\n\nexport class PermissionsChannelClient implements IPermissionsService {\n  constructor(private channel: IChannel) {}\n\n  remove = async (permissions: Permissions): Promise<boolean> => {\n    return this.channel.call('remove', permissions);\n  };\n\n  contains = async (permissions: Permissions): Promise<boolean> => {\n    return this.channel.call('contains', permissions);\n  };\n  request = async (permissions: Permissions): Promise<boolean> => {\n    return this.channel.call('request', permissions);\n  };\n}\n"
  },
  {
    "path": "src/service/preference/browser/preferenceService.ts",
    "content": "import { Inject, Service } from 'typedi';\nimport { observable } from 'mobx';\nimport { ISyncStorageService } from '@/service/common/storage';\nimport { IStorageService } from '@web-clipper/shared/lib/storage';\nimport { IPreferenceService } from '@/service/common/preference';\nimport type { IUserPreference, TIconColor } from '@/service/common/preference';\n\nclass PreferenceService implements IPreferenceService {\n  @observable\n  public userPreference: IUserPreference = {\n    iconColor: 'dark',\n  };\n\n  constructor(@Inject(ISyncStorageService) private syncStorageService: IStorageService) {\n    this.syncStorageService.onDidChangeStorage((e) => {\n      if (e === 'iconColor') {\n        this.userPreference.iconColor = this.getIconColor();\n      }\n    });\n  }\n\n  init = async () => {\n    try {\n      this.userPreference.iconColor = this.getIconColor();\n    } catch (_error) {\n      console.log('Load Config Error');\n    }\n  };\n\n  updateIconColor = async (color: TIconColor) => {\n    await this.syncStorageService.set('iconColor', color);\n  };\n\n  private getIconColor = () => {\n    return (this.syncStorageService.get('iconColor') as 'dark' | 'light' | 'auto' | null) ?? 'auto';\n  };\n}\n\nService(IPreferenceService)(PreferenceService);\n"
  },
  {
    "path": "src/service/request/common/request.test.ts",
    "content": "/**\n * @jest-environment jsdom\n */\nimport { MockRequestService } from '@/__test__/utils';\nimport { RequestHelper } from './request';\n\ndescribe('test RequestHelper', () => {\n  it('test baseURL', () => {\n    const mockRequestService = new MockRequestService(() => {\n      return '';\n    });\n    const request = new RequestHelper({\n      baseURL: 'https://api.clipper.website/',\n      request: mockRequestService,\n    });\n\n    request.get('diamondyuan');\n    expect(mockRequestService.mock.request.mock.calls[0]).toEqual([\n      'https://api.clipper.website/diamondyuan',\n      { method: 'get', headers: {} },\n    ]);\n\n    request.get('https://clipper.website');\n    expect(mockRequestService.mock.request.mock.calls[1]).toEqual([\n      'https://clipper.website/',\n      { method: 'get', headers: {} },\n    ]);\n\n    request.get('http://clipper.website');\n    expect(mockRequestService.mock.request.mock.calls[2]).toEqual([\n      'http://clipper.website/',\n      { method: 'get', headers: {} },\n    ]);\n  });\n\n  it('test post put', () => {\n    const mockRequestService = new MockRequestService(() => {\n      return '';\n    });\n    const request = new RequestHelper({\n      baseURL: 'https://api.clipper.website/',\n      request: mockRequestService,\n    });\n    request.post('DiamondYuan', {\n      data: { name: 'DiamondYuan' },\n    });\n    expect(mockRequestService.mock.request.mock.calls[0]).toEqual([\n      'https://api.clipper.website/DiamondYuan',\n      { method: 'post', requestType: 'json', data: { name: 'DiamondYuan' }, headers: {} },\n    ]);\n\n    const formData = new FormData();\n    formData.set('name', 'DiamondYuan');\n    request.postForm('DiamondYuan', {\n      data: formData,\n    });\n    expect(mockRequestService.mock.request.mock.calls[1]).toEqual([\n      'https://api.clipper.website/DiamondYuan',\n      { method: 'post', requestType: 'form', data: formData, headers: {} },\n    ]);\n\n    request.put('DiamondYuan', {\n      data: { name: 'DiamondYuan' },\n    });\n    expect(mockRequestService.mock.request.mock.calls[2]).toEqual([\n      'https://api.clipper.website/DiamondYuan',\n      { method: 'put', data: { name: 'DiamondYuan' }, headers: {} },\n    ]);\n  });\n\n  it('test header', () => {\n    const mockRequestService = new MockRequestService(() => {\n      return '';\n    });\n    const request = new RequestHelper({\n      baseURL: 'https://api.clipper.website/',\n      headers: {\n        token: '12345',\n      },\n      request: mockRequestService,\n    });\n    request.post('DiamondYuan', {\n      data: { name: 'DiamondYuan' },\n    });\n    expect(mockRequestService.mock.request.mock.calls[0]).toEqual([\n      'https://api.clipper.website/DiamondYuan',\n      {\n        method: 'post',\n        requestType: 'json',\n        data: { name: 'DiamondYuan' },\n        headers: { token: '12345' },\n      },\n    ]);\n\n    request.post('DiamondYuan', {\n      data: { name: 'DiamondYuan' },\n      headers: { token: '123456' },\n    });\n    expect(mockRequestService.mock.request.mock.calls[1]).toEqual([\n      'https://api.clipper.website/DiamondYuan',\n      {\n        method: 'post',\n        requestType: 'json',\n        data: { name: 'DiamondYuan' },\n        headers: { token: '123456' },\n      },\n    ]);\n  });\n});\n\ndescribe('test params', () => {\n  it('support overwrite params', () => {\n    const mockRequestService = new MockRequestService(() => {\n      return '';\n    });\n    const request = new RequestHelper({\n      baseURL: 'https://api.clipper.website/',\n      params: {\n        token: '12345',\n      },\n      request: mockRequestService,\n    });\n    request.post('DiamondYuan?token=123', {\n      data: { name: 'DiamondYuan' },\n    });\n    expect(mockRequestService.mock.request.mock.calls[0]).toEqual([\n      'https://api.clipper.website/DiamondYuan?token=123',\n      {\n        method: 'post',\n        requestType: 'json',\n        data: { name: 'DiamondYuan' },\n        headers: {},\n      },\n    ]);\n  });\n\n  it('support add params', () => {\n    const mockRequestService = new MockRequestService(() => {\n      return '';\n    });\n    const request = new RequestHelper({\n      baseURL: 'https://api.clipper.website/',\n      params: {\n        token: '12345',\n      },\n      request: mockRequestService,\n    });\n    request.post('DiamondYuan?hello=world', {\n      data: { name: 'DiamondYuan' },\n    });\n    expect(mockRequestService.mock.request.mock.calls[0]).toEqual([\n      'https://api.clipper.website/DiamondYuan?hello=world&token=12345',\n      {\n        method: 'post',\n        requestType: 'json',\n        data: { name: 'DiamondYuan' },\n        headers: {},\n      },\n    ]);\n  });\n});\n"
  },
  {
    "path": "src/service/request/common/request.ts",
    "content": "import { ResponseInterceptor } from './../../common/request';\nimport {\n  IGetFormRequestOptions,\n  IHelperOptions,\n  IPostFormRequestOptions,\n  IPostRequestOptions,\n  RequestInterceptor,\n  TRequestOption,\n  IExtendRequestHelper,\n  IPutRequestOptions,\n} from '@/service/common/request';\n\nexport class RequestHelper implements IExtendRequestHelper {\n  constructor(private options: IHelperOptions) {\n    //\n  }\n\n  post<T>(url: string, options: Omit<IPostRequestOptions, 'method' | 'requestType'>) {\n    return this.request<T>(url, {\n      ...options,\n      method: 'post',\n      requestType: 'json',\n    });\n  }\n\n  postForm<T>(url: string, options: Omit<IPostFormRequestOptions, 'method' | 'requestType'>) {\n    return this.request<T>(url, {\n      ...options,\n      method: 'post',\n      requestType: 'form',\n    });\n  }\n\n  put<T>(url: string, options: Omit<IPutRequestOptions, 'method'>) {\n    return this.request<T>(url, {\n      ...options,\n      method: 'put',\n    });\n  }\n\n  get<T>(url: string, options?: Omit<IGetFormRequestOptions, 'method'>) {\n    return this.request<T>(url, {\n      ...options,\n      method: 'get',\n    });\n  }\n\n  private async request<T>(url: string, options: TRequestOption): Promise<T> {\n    let requestUrl = url;\n    let requestOptions = options;\n    let requestInterceptors: RequestInterceptor[] | RequestInterceptor =\n      this.options.interceptors?.request ?? [];\n\n    if (requestInterceptors && !Array.isArray(requestInterceptors)) {\n      requestInterceptors = [requestInterceptors] as RequestInterceptor[];\n    }\n    requestInterceptors = [this.basicRequestInterceptors.bind(this)].concat(requestInterceptors);\n    for (const interceptor of requestInterceptors) {\n      const res = interceptor(requestUrl, requestOptions);\n      requestUrl = res.url ?? requestUrl;\n      requestOptions = res.options ?? requestOptions;\n    }\n    let result = await this.options.request.request<T>(requestUrl, requestOptions);\n\n    let responseInterceptors: ResponseInterceptor[] | ResponseInterceptor =\n      this.options.interceptors?.response ?? [];\n    if (responseInterceptors && !Array.isArray(responseInterceptors)) {\n      responseInterceptors = [responseInterceptors] as ResponseInterceptor[];\n    }\n    for (const interceptor of responseInterceptors) {\n      result = interceptor(result) as T;\n    }\n    return result;\n  }\n\n  private basicRequestInterceptors(\n    url: string,\n    options: TRequestOption\n  ): ReturnType<RequestInterceptor> {\n    let requestUrl = url;\n    if (!this.options.baseURL || url.match(/^https?:\\/\\//)) {\n      requestUrl = url;\n    } else {\n      requestUrl = `${this.options.baseURL}${url}`;\n    }\n    const parsedUrl = new URL(requestUrl);\n    if (this.options.params) {\n      const keys = Object.keys(this.options.params);\n      for (const key of keys) {\n        if (!parsedUrl.searchParams.has(key)) {\n          parsedUrl.searchParams.append(key, this.options.params[key]);\n        }\n      }\n    }\n    return {\n      url: parsedUrl.href,\n      options: {\n        ...options,\n        headers: {\n          ...this.options?.headers,\n          ...options.headers,\n        },\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "src/service/request/tool/basic.ts",
    "content": "import { IPermissionsService } from './../../common/permissions';\nimport { extend, RequestMethod } from 'umi-request';\nimport { IRequestService, IBasicRequestService, TRequestOption } from '@/service/common/request';\nimport Container, { Service } from 'typedi';\nclass BasicRequestService implements IRequestService {\n  private requestMethod: RequestMethod;\n  constructor() {\n    this.requestMethod = extend({});\n  }\n\n  async download(url: string) {\n    const permissionsService = Container.get(IPermissionsService);\n    await permissionsService.request({ origins: [`${new URL(url).origin}/*`] });\n    return new Promise<Blob>(resolve => {\n      let oReq = new XMLHttpRequest();\n      oReq.open('GET', url, true);\n      oReq.responseType = 'blob';\n      oReq.onload = function() {\n        let blob = oReq.response;\n        resolve(blob);\n      };\n      oReq.send();\n    });\n  }\n\n  request(url: string, options: TRequestOption) {\n    switch (options.method) {\n      case 'get': {\n        return this.requestMethod.get(url, {\n          headers: options.headers,\n        });\n      }\n      case 'put': {\n        return this.requestMethod.put(url, {\n          headers: options.headers,\n          data: options.data,\n        });\n      }\n      case 'post': {\n        return this.requestMethod.post(url, {\n          headers: options.headers,\n          data: options.data,\n          requestType: options.requestType,\n        });\n      }\n      default: {\n        throw new Error('Unsupported request method');\n      }\n    }\n  }\n}\n\nService(IBasicRequestService)(BasicRequestService);\n"
  },
  {
    "path": "src/service/tab/browser/background/tabService.ts",
    "content": "import {\n  ITabService,\n  CaptureVisibleTabOptions,\n  AbstractTabService,\n  Tab,\n} from '@/service/common/tab';\nimport * as browser from '@web-clipper/chrome-promise';\nimport { Service } from 'typedi';\n\nclass ChromeTabService extends AbstractTabService {\n  getCurrent() {\n    return new Promise<Tab>(r => {\n      chrome.tabs.query(\n        {\n          currentWindow: true,\n          active: true,\n        },\n        tab => r(tab[0])\n      );\n    });\n  }\n\n  remove(tabId: number): Promise<void> {\n    return browser.tabs.remove(tabId) as Promise<void>;\n  }\n\n  captureVisibleTab(option: CaptureVisibleTabOptions | number) {\n    return browser.tabs.captureVisibleTab(option);\n  }\n\n  sendMessage<T>(tabId: number, message: any): Promise<T> {\n    return browser.tabs.sendMessage<T>(tabId, message);\n  }\n\n  create(createProperties: chrome.tabs.CreateProperties): Promise<chrome.tabs.Tab> {\n    return new Promise<chrome.tabs.Tab>(r => {\n      chrome.tabs.create(createProperties, tab => {\n        r(tab);\n      });\n    });\n  }\n}\n\nService(ITabService)(ChromeTabService);\n"
  },
  {
    "path": "src/service/tab/common/tabIpc.ts",
    "content": "import {\n  ITabService,\n  Tab,\n  CaptureVisibleTabOptions,\n  AbstractTabService,\n} from '@/service/common/tab';\nimport { IServerChannel, IChannel } from '@/service/common/ipc';\n\nexport class TabChannel implements IServerChannel {\n  constructor(private service: ITabService) {}\n\n  callCommand = async (\n    context: chrome.runtime.Port['sender'],\n    command: string,\n    arg: any\n  ): Promise<any> => {\n    switch (command) {\n      case 'getCurrent':\n        return context?.tab;\n      case 'remove':\n        return this.service.remove(arg);\n      case 'captureVisibleTab':\n        return this.service.captureVisibleTab(arg);\n      case 'create':\n        return this.service.create(arg);\n      case 'sendMessage':\n        return this.service.sendMessage(arg[0], arg[1]);\n      default: {\n        throw new Error(`Call not found: ${command}`);\n      }\n    }\n  };\n}\n\nexport class TabChannelClient extends AbstractTabService {\n  constructor(private channel: IChannel) {\n    super();\n  }\n\n  getCurrent = async (): Promise<Tab> => {\n    return this.channel.call('getCurrent');\n  };\n\n  remove = async (tabId: number): Promise<void> => {\n    return this.channel.call('remove', tabId);\n  };\n\n  captureVisibleTab = async (option: CaptureVisibleTabOptions | number) => {\n    return this.channel.call<string>('captureVisibleTab', option);\n  };\n\n  sendMessage = async <T>(tabId: number, message: any) => {\n    return this.channel.call<T>('sendMessage', [tabId, message]);\n  };\n\n  create = async (createProperties: chrome.tabs.CreateProperties) => {\n    return this.channel.call<chrome.tabs.Tab>('create', createProperties);\n  };\n}\n"
  },
  {
    "path": "src/service/webRequest/browser/background/tabService.ts",
    "content": "import {\n  IWebRequestService,\n  RequestInBackgroundOptions,\n  WebBlockHeader,\n  WebRequestBlockOption,\n} from '@/service/common/webRequest';\nimport queryString from 'query-string';\nimport short from 'short-uuid';\nimport request from 'umi-request';\n\nexport class BackgroundWebRequestService implements IWebRequestService {\n  private startCounter: number;\n  private handlerMap: Map<string, { ruleId: number[] }>;\n\n  constructor() {\n    this.handlerMap = new Map<string, any>();\n    this.startCounter = Math.floor(Date.now() / 1000);\n  }\n\n  private getRuleId() {\n    return this.startCounter++;\n  }\n\n  async startChangeHeader(option: WebRequestBlockOption): Promise<WebBlockHeader> {\n    const uuid = short.generate();\n    const modifyHeadersAction: chrome.declarativeNetRequest.RuleAction = {\n      type: 'modifyHeaders' as chrome.declarativeNetRequest.RuleActionType,\n      requestHeaders: option.requestHeaders.map((header) => ({\n        header: header.name,\n        operation: 'set' as chrome.declarativeNetRequest.HeaderOperation,\n        value: header.value,\n      })),\n    } as const;\n    const ruleId = this.getRuleId();\n    const rule: chrome.declarativeNetRequest.Rule = {\n      id: ruleId,\n      priority: 3,\n      action: modifyHeadersAction,\n      condition: {\n        urlFilter: uuid,\n      },\n    };\n    await chrome.declarativeNetRequest.updateDynamicRules({\n      addRules: [rule],\n      removeRuleIds: [],\n    });\n    this.handlerMap.set(uuid, {\n      ruleId: [ruleId],\n    });\n    return {\n      name: uuid,\n      value: uuid,\n    };\n  }\n\n  requestInBackground<T>(url: string, options: RequestInBackgroundOptions) {\n    return request<T>(url, options);\n  }\n\n  async changeUrl(url: string, query: WebBlockHeader): Promise<string> {\n    return queryString.stringifyUrl({ url, query: { [query.name]: query.value } });\n  }\n\n  async end(webBlockHeader: WebBlockHeader): Promise<void> {\n    const handler = this.handlerMap.get(webBlockHeader.value);\n    if (!handler) {\n      return;\n    }\n    chrome.declarativeNetRequest.updateDynamicRules({\n      addRules: [],\n      removeRuleIds: handler.ruleId,\n    });\n  }\n}\n"
  },
  {
    "path": "src/service/webRequest/chrome/background/tabService.ts",
    "content": "import { IWebRequestService } from '@/service/common/webRequest';\nimport { Service } from 'typedi';\nimport { BackgroundWebRequestService } from '@/service/webRequest/browser/background/tabService';\n\nclass ChromeBackgroundWebRequestService extends BackgroundWebRequestService {\n  constructor() {\n    super();\n  }\n}\n\nService(IWebRequestService)(ChromeBackgroundWebRequestService);\n"
  },
  {
    "path": "src/service/webRequest/common/webRequestIPC.ts",
    "content": "import {\n  IWebRequestService,\n  WebRequestBlockOption,\n  WebBlockHeader,\n  RequestInBackgroundOptions,\n} from '@/service/common/webRequest';\nimport { IServerChannel, IChannel } from '@/service/common/ipc';\n\nexport class WebRequestChannel implements IServerChannel {\n  constructor(private service: IWebRequestService) {}\n\n  callCommand = async (\n    _context: chrome.runtime.Port['sender'],\n    command: string,\n    arg: any\n  ): Promise<any> => {\n    switch (command) {\n      case 'end':\n        return this.service.end(arg);\n      case 'startChangeHeader':\n        return this.service.startChangeHeader(arg);\n      case 'requestInBackground':\n        return this.service.requestInBackground(arg[0], arg[1]);\n      case 'changeUrl': {\n        return this.service.changeUrl(arg[0], arg[1]);\n      }\n      default: {\n        throw new Error(`Call not found: ${command}`);\n      }\n    }\n  };\n}\n\nexport class WebRequestChannelClient implements IWebRequestService {\n  constructor(private channel: IChannel) {}\n\n  startChangeHeader = async (option: WebRequestBlockOption): Promise<WebBlockHeader> =>\n    this.channel.call('startChangeHeader', option);\n\n  end = async (webBlockHeader: WebBlockHeader): Promise<void> =>\n    this.channel.call('end', webBlockHeader);\n\n  requestInBackground = async <T>(url: string, options: RequestInBackgroundOptions): Promise<T> =>\n    this.channel.call('requestInBackground', [url, options]);\n\n  changeUrl = async (url: string, query: WebBlockHeader): Promise<string> =>\n    this.channel.call('changeUrl', [url, query]);\n}\n"
  },
  {
    "path": "src/service/worker/common/index.ts",
    "content": "import { Token } from 'typedi';\n\nexport interface IWorkerService {\n  changeIcon(icon: string): Promise<void>;\n\n  initContextMenu(): Promise<void>;\n}\n\nexport const IWorkerService = new Token<IWorkerService>('IWorkerService');\n"
  },
  {
    "path": "src/service/worker/common/workserServiceIPC.ts",
    "content": "import { IChannel, IServerChannel } from '@/service/common/ipc';\nimport { IWorkerService } from '.';\n\nexport class WorkerServiceChannel implements IServerChannel {\n  constructor(private service: IWorkerService) {}\n\n  callCommand = async (_ctx: any, command: string, arg: any): Promise<any> => {\n    switch (command) {\n      case 'changeIcon':\n        return this.service.changeIcon(arg);\n      case 'initContextMenu':\n        return this.service.initContextMenu();\n      default: {\n        throw new Error(`Call not found: ${command}`);\n      }\n    }\n  };\n}\n\nexport class WorkerServiceChannelClient implements IWorkerService {\n  constructor(private channel: IChannel) {}\n\n  changeIcon = async (icon: string) => {\n    return this.channel.call<void>('changeIcon', icon);\n  };\n\n  initContextMenu = async () => {\n    return this.channel.call<void>('initContextMenu');\n  };\n}\n"
  },
  {
    "path": "src/service/worker/worker/workerService.ts",
    "content": "import { IExtensionContainer, IExtensionService } from '@/service/common/extension';\nimport Container, { Service } from 'typedi';\nimport { IWorkerService } from '../common';\nimport { getResourcePath } from '@/common/getResource';\n\nclass WorkerService implements IWorkerService {\n  constructor() {}\n  async changeIcon(iconColor: string): Promise<void> {\n    if (iconColor === 'light') {\n      chrome.action.setIcon({ path: await getResourcePath('icons/icon-dark.png') });\n    } else {\n      chrome.action.setIcon({ path: await getResourcePath('icons/icon.png') });\n    }\n  }\n  async initContextMenu(): Promise<void> {\n    const extensionContainer = Container.get(IExtensionContainer);\n    const extensionService = Container.get(IExtensionService);\n    await extensionContainer.init();\n    await extensionService.init();\n    const contextMenus = extensionContainer.contextMenus;\n    const currentContextMenus = contextMenus.filter(\n      (p) => !extensionService.DisabledExtensionIds.includes(p.id)\n    );\n    chrome.contextMenus.removeAll(() => {\n      for (const iterator of currentContextMenus) {\n        const Factory = iterator.contextMenu;\n        const instance = new Factory();\n        chrome.contextMenus.create({\n          id: iterator.id,\n          title: instance.manifest.name,\n          contexts: instance.manifest.contexts as any[],\n        });\n      }\n    });\n  }\n}\n\nService(IWorkerService)(WorkerService);\n"
  },
  {
    "path": "src/services/account/common.ts",
    "content": "import { AccountPreference } from '@/common/modelTypes/account';\n\nexport function unpackAccountPreference(account: AccountPreference) {\n  const {\n    id,\n    type,\n    defaultRepositoryId,\n    imageHosting,\n    name,\n    avatar,\n    description,\n    homePage,\n    ...info\n  } = account;\n  return {\n    id,\n    account: {\n      type,\n      defaultRepositoryId,\n      imageHosting,\n      info,\n    },\n    userInfo: {\n      name,\n      avatar,\n      description,\n      homePage,\n    },\n  };\n}\n"
  },
  {
    "path": "src/services/configuration/common/configuration.ts",
    "content": "import { Token } from 'typedi';\n\nexport interface IWebClipperConfiguration {\n  yuque_oauth: {\n    clientId: string;\n    callback: string;\n    scope: string;\n  };\n}\nexport interface IConfigurationService {\n  getConfiguration(): IWebClipperConfiguration;\n\n  init(): void;\n}\n\nexport const IConfigurationService = new Token<IConfigurationService>();\n"
  },
  {
    "path": "src/services/configuration/common/configurationService.ts",
    "content": ""
  },
  {
    "path": "src/services/environment/common/changelog/CHANGELOG.en-US.md",
    "content": "## 1.42.0\n\n`2025-10-21`\n\n- Fix Notion formatting issues\n- Fix workspace name display errors\n- Improve Notion account verification\n- Fix wolai upload problems\n\n## 1.41.0\n\n`2025-06-02`\n\n- Fix dida365 error\n- Fix Yuque not showing in TOC issue\n- Support memos\n- Add filename support for piclist image hosting\n- Fix Notion error\n\n## 1.39.0\n\n`2024-09-25`\n\n- Support BuildinAI\n\n## 1.38.0\n\n`2024-07-28`\n\n- Support obsidian\n\n## 1.37.4\n\n`2024-07-17`\n\n- fix siyuan image upload issue.\n\n## 1.37.3\n\n`2024-06-26`\n\n- Fix FlowUS bind error\n\n## 1.37.2\n\n`2024-06-24`\n\n- Fix the problem of formatting error after saving to notion.\n\n## 1.37.1\n\n`2024-06-22`\n\n- Migrate to Manifest V3\n- Support tencent cos as imangeHost\n\n## 1.36.0\n\n`2023-11-14`\n\n- Add German language\n- Fix wolai issue\n\n## 1.35.0\n\n`2023-08-06`\n\n- Optimized the support for Webdav.\n\n## 1.34.0\n\n`2023-07-01`\n\n- ✨ Removed Powerpack and account system\n\n  - Removed email sending\n  - Removed sending to Kindle\n  - Removed OCR\n\n- ✨ Removed all remote data\n\n  - Changelog\n  - Privacy policy\n  - Remote configuration file\n\n## 1.33.0\n\n`2022-12-15`\n\n- 🐛 fix: import to flowus with html content\n\n## 1.32.0\n\n`2022-08-11`\n\n- 🐛 fix: fix notion auth problem\n\n## 1.31.0\n\n`2022-08-11`\n\n- 🐛 feat: support flowus\n\n## 1.30.0\n\n`2021-3-26`\n\n- ✨ support config icon color\n- ✨ support save selection\n\n## 1.29.0\n\n`2021-1-27`\n\n- ✨ support leanote\n- ✨ support flomo\n- ✨ github imageHosting support config branch\n- 🐛 fix notion 415 error\n- 🐛 Fixed the bug that WebDAV requires powerpack (😄 It's really a bug)\n\nThanks [PascalNoisette](https://github.com/PascalNoisette)\n\n## 1.28.4\n\n`2020-12-16`\n\n- 🐛 Fix the problem of joplin 1.14 page stuck\n\n## 1.28.3\n\n`2020-12-15`\n\n- ✨ support joplin 1.14+\n\n## 1.28.2\n\n`2020-12-02`\n\n- ✨ Support Github repository\n- ✨ Support Github imageHosting\n- 💄 show loading when ocr\n\nThanks @Touma-Kazusa and @Okami Wong\n\n## 1.28.1\n\n`2020-11-26`\n\n- 🐛 Fix the style of area selector (OCR ScreenShoot). The selector will not be obscured by other elements\n\n## 1.28.0\n\n`2020-11-24`\n\n- 🌍 Support zh-TW\n- 🐛 Fix the problem that wolai and wotion cannot be used when third-party cookies are turned off.\n\n## 1.28.0\n\n`2020-09-23`\n\n- 🐛 Fix the problem that wolai cannot be used\n\n## 1.27.0\n\n`2020-09-23`\n\n- 🐛 Fix the problem that wolai cannot be used\n\n## 1.26.1\n\n`2020-08-27`\n\n- ✨ support vivaldi\n\n## 1.26.0\n\n`2020-08-14`\n\n- ✨ Support wolai\n- ✨ Support baklib\n- ✨ ~~support vivaldi~~\n\n## 1.25.0\n\n`2020-07-01`\n\n- ✨ Support notion database\n- 🔖 Release canary version\n\n## 1.24.0\n\n`2020-04-28`\n\n- ✨ Support wiznote\n\n## 1.23.0\n\n`2020-04-07`\n\n- 🐛 Fixed an issue where some plugins disappeared\n- 💬 Add notice for notion.\n\n## 1.22.0\n\n`2020-03-28`\n\n- 🐛 Fix OneNote oauth,support business account\n\n## 1.21.0\n\n`2020-03-28`\n\n- ✨ Support zhihu gif\n- ♻️ Move icon to local\n- ♻️ Remove extensions store\n- 🐛 Fix sm.ms\n\n## 1.20.0\n\n`2020-02-25`\n\n- ✨ Support dida365\n- ✨ Support TickTick\n- ✨ Support Webdev (Dropbox)\n\n## 1.19.0\n\n`2020-01-29`\n\n- ✨ Support confluence\n- ✨ Support ulysses\n\n## 1.18.0\n\n`2020-01-20`\n\n- 💄 Optimize UI for account selection\n- 💄 Add mask for web clipper\n- 💄 update powerpack ui\n\n## 1.17.0\n\n`2020-01-08`\n\n- ✨ Support firefox\n\n## 1.16.0\n\n`2019-12-30`\n\n- ✨ Support Joplin\n- 💄 Add loading page\n- 🐛 Fix web clipper switch error.\n\n## 1.15.1\n\n`2019-12-15`\n\n- ✨ Reduce permissions required by web clipper.\n\n## 1.15.0\n\n`2019-12-14`\n\n- ✨ Yuque(oauth) support built in image hosting\n- ✨ Support server chan\n\n## 1.14.1\n\n`2019-12-05`\n\n- 🐛 Fix repository search.\n\n## 1.14.0\n\n`2019-12-05`\n\n- ✨ Github support config label and filter repository;\n- ✨ Yuque support config slag and filter repository;\n- ✨ Dark mode supports automatic icon switching.\n- 🐛 When there is no account, there will be a reminder on the close settings page.\n\n### Powerpack\n\n- ✨ Support send to kindle.\n\n## 1.13.0\n\n`2019-11-08`\n\n- ✨ Support imgur.\n- 🐛 Upgrade turndown.\n- 💄 Fix icon of bear and OneNote and sm.ms\n- 💄 Add title when select account.\n- 💄 Fix style of area selector.\n\n### Powerpack\n\n- ✨ Support OCR\n\n### Extension API\n\n- ✨ Support automatic extensions\n- ✨ Support pangu\n\n## 1.12.0\n\n`2019-10-28`\n\n- ✨ Support Powerpack.\n- 📝 Add Privacy policy.\n\n### Powerpack\n\n- ✨ Support Save to Email\n\n### Extension API\n\n- ✨ Support Download File\n- ✨ Support antd\n\n## 1.11.0\n\n`2019-09-30`\n\n- ✨ Support for extended installation, disabling and deletion.\n- ✨ Extended API supports copying content to clipboard.\n- 🎨 Some built-in extensions become optional installation.\n- 💄 Extensions page button to add more prompt.\n- 🐛 Fixed a problem where the oauth of the yuque user with the enterprise space failed.\n- 🐛 Fixed an issue where OneNote chose the knowledge base not work.\n- 🐛 Fixed image clipping failure for relative path/\n\n## 1.10.0\n\n`2019-09-19`\n\n- ✨ Support share clip content to weibo twitter and douban.\n- 💄 More icon.\n- 🌐 Easier to add translation.\n\n## 1.9.2\n\n`2019-09-11`\n\n- 🐛 Fix default account and add new oauth account not work when enable default extension.\n- 🌐 Support ja-JP.\n- 💄 fix style of locale selector.\n\n## 1.9.1\n\n`2019-09-06`\n\n- 🐛 Fix oauth failed in firefox.\n- 🐛 Fix infinite loop request when edit account.\n\n## 1.9.0\n\n`2019-09-02`\n\n- ✨ Support Bear.\n- 📝 Add unauthorized error message for notion and youdao.\n\n## 1.8.0\n\n`2019-09-01`\n\n- ✨ Support OneNote.\n- ✨ Support display changelog.\n- 🐛 fix yuque oauth error.\n- 🐛 fix load imageHosting failed.\n- 📝 add tooltip for set default clip extension.\n\n## 1.7.3 & 1.7.2\n\n`2019-08-24`\n\n- ✨ Yuque support oauth.\n- 🐛 Fixed description of the update page.\n- 🐛 Fix crash when local is not zh-CN or en-US\n- 🐛 Fixed okText of account modal.\n- 🐛 Fix full page extension.\n- 💄 Add Icon For Setting Page.\n- 💄 Change name of notion.\n- 🌐 Add more translate.\n\n## 1.7.1\n\n`2019-08-18`\n\n- 🐛 Fixed an issue where the WYSIWYG mode could not be properly switched.\n"
  },
  {
    "path": "src/services/environment/common/changelog/CHANGELOG.zh-CN.md",
    "content": "## 1.42.0\n\n`2025-10-21`\n\n- 修复 Notion 格式问题\n- 修复工作区名称显示错误\n- 改进 Notion 账户验证\n- 修复 wolai 上传问题\n\n## 1.41.0\n\n`2025-06-02`\n\n- 修复 dida365 的报错\n- 修复 Yuque 不显示在 TOC 的问题\n- 支持 memos\n- 上传piclist图床增加filename\n- 修复 Notion 保存的问题\n\n## 1.39.0\n\n`2024-09-25`\n\n- 支持 BuildinAI\n\n## 1.38.0\n\n`2024-07-28`\n\n- 支持 obsidian\n\n## 1.37.4\n\n`2024-07-17`\n\n- 修复思源上传图片的问题\n\n## 1.37.3\n\n`2024-06-26`\n\n- Fix 修复 FlowUS 绑定错误\n\n## 1.37.2\n\n`2024-06-24`\n\n- 修复保存到 Notion 后格式错误的问题\n\n## 1.37.1\n\n`2024-06-22`\n\n- 迁移到 Manifest V3\n- 支持腾讯云对象存储作为图床\n\n## 1.36.0\n\n`2023-11-14`\n\n- 新增德语\n- 修复无法保存到 wolai 的问题\n\n## 1.35.0\n\n`2023-08-06`\n\n- 优化了 Webdav 的支持\n\n## 1.34.0\n\n`2023-07-01`\n\n- ✨ 移除了 Powerpack 和账户系统\n\n  - 删除了发送邮件\n  - 删除了发送到 kindle\n  - 删除了 ocr 功能\n\n- ✨ 移除了所有从服务器获取的数据，改为从本地获取\n\n  - 更新日志\n  - 隐私协议\n  - 远程的配置文件\n\n## 1.33.0\n\n`2022-12-15`\n\n- 🐛 优化导入到 flowus 的文件格式\n\n## 1.32.0\n\n`2022-08-11`\n\n- 🐛 修复 notion 登录的问题\n\n## 1.31.0\n\n`2022-08-11`\n\n- ✨ 支持 flowus\n\n## 1.30.0\n\n`2021-3-27`\n\n- ✨ 支持定义 icon 颜色\n- ✨ 支持保存选择的内容\n\n## 1.29.0\n\n`2021-1-27`\n\n- ✨ 支持蚂蚁笔记 @PascalNoisette\n- ✨ 支持浮墨笔记\n- ✨ GitHub 图床支持设置分支\n- 🐛 修复 Notion Http 415 Error\n- 🐛 修复了 WebDAV 需要付费的 bug (😄 真的是一个 bug)\n\n非常感谢 [PascalNoisette](https://github.com/PascalNoisette) 的贡献\n\n## 1.28.4\n\n`2020-12-16`\n\n- 🐛 修复 joplin 1.14 版本卡死的问题\n\n## 1.28.3\n\n`2020-12-15`\n\n- ✨ 支持 joplin 1.14+\n\n## 1.28.2\n\n`2020-12-02`\n\n- ✨ 支持保存到 Github 代码仓库\n- ✨ 支持保存到 Github 图床\n- 💄 OCR 的时候显示 loading\n\n非常感谢 @Touma-Kazusa 和 @Okami Wong 的贡献\n\n## 1.28.1\n\n`2020-11-26`\n\n- 🐛 修改选择器（文字识别，截图）的样式, 选择器在选择的时候将不会被其他元素遮挡。\n\n## 1.28.0\n\n`2020-11-24`\n\n- 🌍 支持繁体中文\n- 🐛 修复了禁用三方 cookie 后 wolai notion 无法使用的问题\n\n## 1.27.0\n\n`2020-09-24`\n\n- 🐛 修复 wolai 无法使用的问题\n\n## 1.26.1\n\n`2020-08-27`\n\n- ✨ support vivaldi\n\n## 1.26.0\n\n`2020-08-14`\n\n- ✨ 支持 wolai（我来）\n- ✨ 支持 baklib\n- ✨ ~~support vivaldi~~\n\n## 1.25.0\n\n`2020-07-01`\n\n- ✨ 支持 notion 的 database\n- 🔖 新增了 canary 发布\n\n## 1.24.0\n\n`2020-04-28`\n\n- ✨ 支持为知笔记\n\n## 1.23.0\n\n`2020-04-07`\n\n- 🐛 修复某些插件丢失的问题。\n- 💬 给 Notion 增加一些提示。\n\n## 1.22.0\n\n`2020-03-28`\n\n- 🐛 修复 OneNote 授权的问题，支持商业版账户\n\n## 1.21.0\n\n`2020-03-28`\n\n- ✨ 支持保存知乎的 gif\n- ♻️ 把 icon 移动到本地\n- ♻️ 删除远程脚本\n- 🐛 修复 sm.ms 秃疮\n\n## 1.20.0\n\n`2020-02-25`\n\n- ✨ 支持 滴答清单\n- ✨ 支持 TickTick（滴答清单海外版)\n- ✨ 支持 Webdev (比如坚果云)\n\n## 1.19.0\n\n`2020-01-29`\n\n- ✨ 支持 confluence\n- ✨ 支持 ulysses\n\n## 1.18.0\n\n`2020-01-20`\n\n- 💄 优化账户选择的 UI\n- 💄 增加灰色的背景和修改 UI\n- 💄 更新加强包的 UI\n\n## 1.17.0\n\n`2020-01-08`\n\n- ✨ 支持火狐浏览器\n\n## 1.16.0\n\n`2019-12-30`\n\n- ✨ 支持 Joplin\n- 💄 开启时增加 loading\n- 🐛 修复插件开关错误的问题\n\n## 1.15.1\n\n`2019-12-15`\n\n- ✨ 缩减插件所需要的权限\n\n## 1.15.0\n\n`2019-12-14`\n\n- ✨ 语雀 (一键授权) 支持内置图床\n- ✨ 支持发送到微信 (server 酱)\n\n## 1.14.1\n\n`2019-12-05`\n\n- 🐛 修复搜索仓库无效的问题。\n\n## 1.14.0\n\n`2019-12-05`\n\n- ✨ Github 支持设置标签以及筛选仓库。\n- ✨ 语雀支持设置路径以及筛选仓库。\n- ✨ 暗黑模式支持自动切换 icon。\n- 🐛 没有账户时，关闭设置页面会有提醒。\n\n### 加强包功能\n\n- ✨ 支持保存到 kindle。\n\n## 1.13.0\n\n`2019-11-08`\n\n- ✨ 支持 imgur.\n- 🐛 升级 turndown。\n- 💄 修复 bear , OneNote 和 sm.ms 的图标。\n- 💄 选择账户的下拉框增加提示。\n- 💄 修复截图选择框的样式。\n\n### 加强包功能\n\n- ✨ 支持文字识别。\n\n### 扩展 API\n\n- ✨ 支持自动化执行插件。\n- ✨ 支持盘古。\n\n## 1.12.0\n\n`2019-10-28`\n\n- ✨ 支持加强包。\n- 📝 增加隐私政策。\n\n### 加强包功能\n\n- ✨ 支持保存文章到邮件。\n\n### 扩展 API\n\n- ✨ 支持下载文件。\n- ✨ 支持 antd。\n\n## 1.11.0\n\n`2019-09-30`\n\n- ✨ 支持扩展的安装，禁用和删除。\n- ✨ 扩展 API 支持复制内容到剪切板。\n- 🎨 部分内置扩展变为可选安装。\n- 💄 扩展管理页面的按钮增加提示。\n- 🐛 修复了拥有企业空间的语雀用户一键授权失败的问题。\n- 🐛 修复了 Onenote 选择知识库无效的问题。\n- 🐛 修复了相对路径的图片剪藏失败的问题。\n\n## 1.10.0\n\n`2019-09-19`\n\n- ✨ 支持分享剪藏的内容到推特、微博和豆瓣。\n- 💄 支持更多图标。\n- 🌐 添加翻译更简单。\n\n## 1.9.2\n\n`2019-09-11`\n\n- 🐛 修复了 开启默认插件时，添加新授权账户以及默认账户失效的问题。\n- 🌐 支持日语\n- 💄 修复了语言选择框的样式\n\n## 1.9.1\n\n`2019-09-06`\n\n- 🐛 修复 Firefox 上授权失败的问题。\n- 🐛 修复编辑账户时无限请求后台导致页面崩溃的问题。\n\n## 1.9.0\n\n`2019-09-02`\n\n- ✨ 支持 Bear\n- 📝 为 Notion 和 有道云笔记添加了未登录时的错误提示。\n\n## 1.8.0\n\n`2019-09-01`\n\n- ✨ 支持 OneNote。\n- ✨ 支持显示更新记录。\n- 🐛 修复语雀授权失败的问题。\n- 🐛 修复加载图床失败的问题。\n- 📝 添加如何设置默认剪藏扩展到提示。\n\n## 1.7.3 & 1.7.2\n\n`2019-08-24`\n\n- ✨ 语雀支持 Oauth 登录\n- 🐛 修复更新页面的提示。\n- 🐛 修复插件在语言不是 zh-CN 或者 en-US 时崩溃的问题。\n- 🐛 修复创建/修改账户时，确认按钮的文案。\n- 🐛 修复 `整个页面` 插件的问题。\n- 💄 设置页面增加 Icon.\n- 💄 修改 Notion 的名字\n- 🌐 添加更多的翻译。\n\n## 1.7.1\n\n`2019-08-18`\n\n- 🐛 修复不能正确开关所见即所得模式的问题.\n"
  },
  {
    "path": "src/services/environment/common/environment.ts",
    "content": "import { Token } from 'typedi';\n\nexport const IEnvironmentService = new Token<IEnvironmentService>();\n\nexport interface IEnvironmentService {\n  privacy(): Promise<string>;\n  changelog(): Promise<string>;\n}\n"
  },
  {
    "path": "src/services/environment/common/environmentService.ts",
    "content": "import { ILocaleService } from '@/service/common/locale';\nimport { Inject, Service } from 'typedi';\nimport { IEnvironmentService } from './environment';\n\n//@ts-ignore\nimport ChangelogEnUS from './changelog/CHANGELOG.en-US.md';\n//@ts-ignore\nimport ChangelogZhCN from './changelog/CHANGELOG.zh-CN.md';\n//@ts-ignore\nimport PrivacyEnUS from './privacy/PRIVACY.en-US.md';\n//@ts-ignore\nimport PrivacyZhCN from './privacy/PRIVACY.zh-CN.md';\n\nconst privacyLocale = {\n  'en-US': PrivacyEnUS,\n  'zh-CN': PrivacyZhCN,\n} as const;\n\nconst changelogLocale = {\n  'en-US': ChangelogEnUS,\n  'zh-CN': ChangelogZhCN,\n} as const;\n\ntype Locale = 'en-US' | 'zh-CN';\n\nfunction keys<T>(data: T): (keyof T)[] {\n  return (Object.keys(data as any) as any) as (keyof T)[];\n}\n\nexport class EnvironmentService implements IEnvironmentService {\n  constructor(@Inject(ILocaleService) private localeService: ILocaleService) {}\n\n  async privacy(): Promise<string> {\n    let workLocale: Locale = 'en-US';\n    if (keys(privacyLocale).some(o => o === this.localeService.locale)) {\n      workLocale = this.localeService.locale as Locale;\n    }\n    return privacyLocale[workLocale];\n  }\n\n  async changelog(): Promise<string> {\n    let workLocale: Locale = 'en-US';\n    if (Object.keys(changelogLocale).some(o => o === this.localeService.locale)) {\n      workLocale = this.localeService.locale as Locale;\n    }\n    return changelogLocale[workLocale];\n  }\n}\n\nService(IEnvironmentService)(EnvironmentService);\n"
  },
  {
    "path": "src/services/environment/common/privacy/PRIVACY.en-US.md",
    "content": "# Privacy policy\n\nAs users of many other internet services, we recognizes that privacy is extremely important. Further details of the Web Clipper's privacy policy are outlined below.\n\n## Where is your Data Saved\n\nServers of Web Clipper are located at Mongodb Cloud and zeit.co in the United States of America.\n\n## Data We Collect\n\nWeb Clipper collects **email address** when you register for Web Clipper service or otherwise voluntarily provide such information. **We don't collect other personal information.**\n\nWeb Clipper uses Github OAuth to register a Web Clipper account from your Github account. We don’t save password and other personal information to Web Clipper. So, no worry about password leak.\n\nWeb Clipper uses cookies and other technologies to enhance your online experience and to learn about how you use the service in order to improve the quality of our services.\n\n## Use\n\nThe log information is mainly used for usage analysis in Google Analytics, such as how many users access X feature etc.We don't collect and  analysis personal information. We may also use log information for auditing, research and analysis to operate and improve Web Clipper technologies and services. If information is used in any way other than indicated above, it will be in the aggregated form, and with all personal identifications removed. Web Clipper will not disclose information users store in the servers to any third party.\n\n## Security\n\nWeb Clipper takes precautions to insure that member account information is kept private. We use reasonable measures to protect member information that is stored within our database, and we restrict access to member information to those employees who need access to perform their job functions, such as our customer service personnel and technical staff. Please note that we cannot guarantee the security of member account information. Unauthorized entry or use, hardware or software failure, and other factors may compromise the security of member information at any time.\n\n### Changes To This Privacy Policy\n\nChanges to this Privacy Policy are effective when they are posted on this page. History of changes will be kept in this [repository](https://github.com/webclipper/web-clipper).\n\n###\n\nPlease contact us at **admin@diamonyuan.com** with any questions regarding our privacy policy.\n"
  },
  {
    "path": "src/services/environment/common/privacy/PRIVACY.zh-CN.md",
    "content": "# 隐私政策\n\n作为许多其他互联网服务的用户，我们认识到隐私非常重要。下面概述了 Web Clipper 隐私策略的更多详细信息。\n\n## 您的数据保存在哪里\n\nWeb Clipper 的服务器位于美利坚合众国的 Mongodb Cloud 和 zeit.co。\n\n## 我们收集的数据\n\n当您注册 Web Clipper 服务或以其他方式自愿提供此类信息时，Web Clipper 只会收集“电子邮件地址”。 **我们不会收集其他个人信息。**\n\nWeb Clipper 使用 Github OAuth 从您的 Github 帐户注册 Web Clipper 帐户。我们不会将密码和其他信息保存到 Web Clipper。因此，无需担心密码泄漏。\n\nWeb Clipper 使用 cookie 和其他技术来增强您的在线体验，并了解您如何使用该服务，从而提高我们的服务质量。\n\n## 使用\n\n日志信息主要用于 Google Analytics（分析）中的使用情况分析，例如有多少用户访问 X 功能等。我们不会收集和分析个人信息。我们还可能将日志信息用于审核，研究和分析，以操作和改进 Web Clipper 技术和服务。如果上述信息以外的任何其他方式使用信息，则该信息将采用汇总形式，并删除所有个人身份信息。 Web Clipper 不会将用户存储在服务器中的信息透露给任何第三方。\n\n## 安全\n\nWeb Clipper 采取了预防措施，以确保成员帐户信息保持私有。我们使用合理的措施来保护存储在数据库中的会员信息，并且将访问会员信息的权限限制为需要访问才能执行其工作职能的那些员工，例如我们的客户服务人员和技术人员。请注意，我们不能保证会员帐户信息的安全性。未经授权的输入或使用，硬件或软件故障以及其他因素可能随时损害成员信息的安全性。\n\n## 本隐私政策的变更\n\n在本页面上发布对本隐私政策的更改后，这些更改才生效。更改历史记录将保存在此 [存储库](https://github.com/webclipper/web-clipper) 中。\n\n如果对我们的隐私政策有任何疑问，请通过**admin@diamonyuan.com**与我们联系。\n"
  },
  {
    "path": "src/services/log/common/index.ts",
    "content": "export enum LogLevel {\n  Trace,\n  Debug,\n  Info,\n  Warning,\n  Error,\n  Critical,\n  Off,\n}\n\nexport const DEFAULT_LOG_LEVEL: LogLevel = LogLevel.Info;\n\nexport interface ILogger {\n  getLevel(): LogLevel;\n  setLevel(level: LogLevel): void;\n\n  trace(message: string, ...args: any[]): void;\n  debug(message: string, ...args: any[]): void;\n  info(message: string, ...args: any[]): void;\n  warn(message: string, ...args: any[]): void;\n  error(message: string | Error, ...args: any[]): void;\n  critical(message: string | Error, ...args: any[]): void;\n\n  flush(): void;\n}\n"
  },
  {
    "path": "src/setupTests.ts",
    "content": "import 'reflect-metadata';\n"
  },
  {
    "path": "src/vendor/global.d.ts",
    "content": "declare module '*.less';\ndeclare module '*.png';\ndeclare module '@web-clipper/readability';\ndeclare module 'turndown-plugin-gfm';\ndeclare module '@web-clipper/remark-pangu';\ndeclare module 'dva-loading';\n\ntype PromiseType<T extends Promise<any>> = T extends Promise<infer U> ? U : never;\n\ntype Unpack<T> = T extends Promise<infer U> ? U : T;\n// eslint-disable-next-line no-unused-vars\ntype CallResult<T extends (...args: any[]) => any> = Unpack<ReturnType<T>>;\n\ninterface Type<T> extends Function {\n  new (...args: any[]): T;\n}\n\n/// <reference path=\"../../node_modules/@types/chrome/index.d.ts\"/>\n\ninterface Window {\n  _gaq: string[][];\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"emitDecoratorMetadata\": true,\n    \"resolveJsonModule\": true,\n    \"experimentalDecorators\": true,\n    \"target\": \"ES2018\",\n    \"module\": \"CommonJS\",\n    \"declaration\": false,\n    \"noImplicitAny\": true,\n    \"strictNullChecks\": true,\n    \"rootDir\": \".\",\n    \"outDir\": \"lib\",\n    \"strict\": true,\n    \"removeComments\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"moduleResolution\": \"node\",\n    \"sourceMap\": true,\n    \"lib\": [\"dom\", \"es2020\", \"es5\", \"ESNext.String\"],\n    \"inlineSources\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n      \"common/*\": [\"src/common/*\"],\n      \"components/*\": [\"src/components/*\"],\n      \"pageActions/*\": [\"src/actions/*\"],\n      \"extensions/*\": [\"src/extensions/*\"],\n      \"browserActions/*\": [\"src/browser/actions/*\"]\n    }\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"./node_modules/*\", \"lib\", \"es\"]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\nimport path from 'path';\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    testTimeout: 10000,\n    setupFiles: path.resolve(__dirname, 'src/setupTests.ts'),\n  },\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, './src'),\n    },\n  },\n});\n"
  },
  {
    "path": "webpack/plugin/webpack-create-extension-manifest-plugin.js",
    "content": "const path = require('path');\nconst fsP = require('fs/promises');\n\nasync function getPackageJsonVersion() {\n  const packageJsonPath = path.join(__dirname, '../../package.json');\n  const packageJson = JSON.parse(await fsP.readFile(packageJsonPath, 'utf8'));\n  return packageJson.version;\n}\n\nclass WebpackCreateExtensionManifestPlugin {\n  constructor({ output, extra }) {\n    this.options = { output, extra };\n  }\n\n  apply(compiler) {\n    compiler.hooks.done.tapPromise('WebpackCreateExtensionManifestPlugin', async () => {\n      const { output } = this.options;\n\n      const chromeManifest = path.join(output, 'chrome/manifest.json');\n      const firefoxManifest = path.join(output, 'manifest.json');\n      await fsP.mkdir(path.dirname(chromeManifest), { recursive: true });\n      await fsP.writeFile(chromeManifest, JSON.stringify(await getChromeManifest(), null, 2));\n      await fsP.writeFile(firefoxManifest, JSON.stringify(await getFirefoxManifest(), null, 2));\n    });\n  }\n}\n\nmodule.exports = { WebpackCreateExtensionManifestPlugin };\n\nasync function getChromeManifest() {\n  return {\n    manifest_version: 3,\n    name: 'Web Clipper',\n    version: await getPackageJsonVersion(),\n    action: {},\n    background: {\n      service_worker: './background.js',\n    },\n    icons: {\n      128: 'icons/icon.png',\n    },\n    commands: {\n      'save-selection': {\n        suggested_key: {\n          default: 'Alt+S',\n        },\n        description: 'Save selection',\n      },\n    },\n    web_accessible_resources: [\n      {\n        resources: ['tool.html', 'tool.js', 'vendor.js'],\n        matches: ['<all_urls>'],\n      },\n    ],\n    content_scripts: [\n      {\n        matches: ['<all_urls>'],\n        js: ['./content_script.js'],\n      },\n    ],\n    host_permissions: ['https://api.clipper.website/*', 'https://resource.clipper.website/*'],\n    optional_host_permissions: ['https://*/*', 'http://*/*', '<all_urls>'],\n    optional_permissions: ['cookies'],\n    permissions: ['activeTab', 'storage', 'contextMenus', 'declarativeNetRequestWithHostAccess'],\n  };\n}\n\nasync function getFirefoxManifest() {\n  return {\n    manifest_version: 3,\n    name: 'Web Clipper',\n    version: await getPackageJsonVersion(),\n    action: {},\n    background: {\n      scripts: ['chrome/background.js'],\n    },\n    icons: {\n      128: 'chrome/icons/icon.png',\n    },\n    commands: {\n      'save-selection': {\n        suggested_key: {\n          default: 'Alt+S',\n        },\n        description: 'Save selection',\n      },\n    },\n    browser_specific_settings: {\n      gecko: {\n        id: 'web-clipper@web-clipper',\n      },\n    },\n    web_accessible_resources: [\n      {\n        resources: ['chrome/tool.html', 'chrome/tool.js', 'chrome/vendor.js'],\n        matches: ['<all_urls>'],\n      },\n    ],\n    content_scripts: [\n      {\n        matches: ['http://*/*', 'https://*/*'],\n        js: ['chrome/content_script.js'],\n      },\n    ],\n    host_permissions: ['<all_urls>', 'http://*/*', 'https://*/*'],\n    permissions: [\n      'cookies',\n      'activeTab',\n      'storage',\n      'contextMenus',\n      'declarativeNetRequestWithHostAccess',\n    ],\n  };\n}\n"
  },
  {
    "path": "webpack/webpack.common.js",
    "content": "const path = require('path');\nconst CopyWebpackPlugin = require('copy-webpack-plugin');\nconst CleanWebpackPlugin = require('clean-webpack-plugin');\nconst webpack = require('webpack');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst {\n  WebpackCreateExtensionManifestPlugin,\n} = require('./plugin/webpack-create-extension-manifest-plugin');\nconst fs = require('fs');\n\nfunction resolve(dir) {\n  return path.join(__dirname, '..', dir);\n}\n\nconst distFiles = fs.readdirSync(resolve('dist')).filter((o) => o !== '.gitkeep');\n\nmodule.exports = {\n  entry: {\n    tool: resolve('src/main/tool.main.chrome.ts'),\n    content_script: resolve('src/main/contentScript.main.ts'),\n    background: resolve('src/main/background.worker.ts'),\n  },\n  output: {\n    path: resolve('dist/chrome'),\n    filename: '[name].js',\n  },\n  optimization: {\n    splitChunks: {\n      cacheGroups: {\n        vendor: {\n          test: /[\\\\/]node_modules[\\\\/](react\\/|react-dom|antd|lodash|@ant-design)[\\\\/]/,\n          name: 'vendor',\n          chunks(chunk) {\n            return chunk.name !== 'background';\n          },\n        },\n      },\n    },\n  },\n  resolve: {\n    alias: {\n      '@': resolve('src/'),\n      common: resolve('src/common/'),\n      components: resolve('src/components/'),\n      browserActions: resolve('src/browser/actions/'),\n      pageActions: resolve('src/actions'),\n      extensions: resolve('src/extensions/'),\n    },\n    extensions: ['.ts', '.tsx', '.js', 'less'],\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.md$/,\n        use: 'raw-loader',\n      },\n      {\n        test: /\\.(jsx|tsx|js|ts)$/,\n        loader: 'ts-loader',\n        options: {\n          transpileOnly: true,\n          getCustomTransformers: () => ({\n            before: [],\n          }),\n          compilerOptions: {\n            module: 'esnext',\n          },\n        },\n        exclude: /node_modules/,\n      },\n      {\n        include: /hypermd|codemirror/,\n        test: [/\\.css$/],\n        use: [\n          {\n            loader: 'style-loader',\n          },\n          {\n            loader: 'css-loader',\n          },\n        ],\n      },\n      {\n        test: /\\.(png|jpg|gif)$/,\n        use: [\n          {\n            loader: 'url-loader',\n            options: {\n              limit: 8192,\n            },\n          },\n        ],\n      },\n      {\n        test: /\\.less$/,\n        include: /node_modules\\/antd|@ant-design|@formily/,\n        use: [\n          {\n            loader: 'style-loader',\n          },\n          {\n            loader: 'css-loader',\n          },\n          {\n            loader: 'less-loader',\n            options: {\n              lessOptions: {\n                modifyVars: {\n                  '@body-background': 'transparent',\n                },\n                javascriptEnabled: true,\n              },\n            },\n          },\n        ],\n      },\n      {\n        test: /\\.less$/,\n        exclude: /node_modules/,\n        use: [\n          {\n            loader: 'style-loader',\n          },\n          {\n            loader: 'css-loader',\n            options: {\n              modules: true,\n              camelCase: true,\n              localIdentName: '[path][name]__[local]--[hash:base64:5]',\n            },\n          },\n          {\n            loader: 'less-loader',\n            options: {\n              lessOptions: {\n                modifyVars: {\n                  '@body-background': 'transparent',\n                },\n                javascriptEnabled: true,\n              },\n            },\n          },\n        ],\n      },\n    ],\n  },\n  plugins: [\n    new webpack.ProvidePlugin({\n      $: 'jquery',\n      jQuery: 'jquery',\n    }),\n    new CleanWebpackPlugin(\n      distFiles.map((p) => `dist/${p}`),\n      {\n        root: path.resolve(__dirname, '../'),\n        verbose: true,\n      }\n    ),\n    new CopyWebpackPlugin([\n      {\n        from: resolve('chrome/html'),\n        to: resolve('dist/chrome'),\n        ignore: ['.*'],\n      },\n      {\n        from: resolve('chrome/js'),\n        to: resolve('dist/chrome'),\n        ignore: ['.*'],\n      },\n      {\n        from: resolve('chrome/icons'),\n        to: resolve('dist/chrome/icons'),\n        ignore: ['.*'],\n      },\n    ]),\n    new WebpackCreateExtensionManifestPlugin({\n      output: resolve('dist'),\n    }),\n    new HtmlWebpackPlugin({\n      title: 'Web Clipper',\n      filename: resolve('dist/chrome/tool.html'),\n      chunks: ['tool'],\n      template: 'src/index.html',\n    }),\n  ].filter((plugin) => !!plugin),\n};\n"
  },
  {
    "path": "webpack/webpack.dev.js",
    "content": "process.env.NODE_ENV = 'development';\n\nconst merge = require('webpack-merge');\nconst common = require('./webpack.common.js');\n\nmodule.exports = merge(common, {\n  devtool: 'source-map',\n  mode: 'development',\n  watchOptions: {\n    ignored: /dist/,\n  },\n});\n"
  },
  {
    "path": "webpack/webpack.prod.js",
    "content": "const merge = require('webpack-merge');\nconst common = require('./webpack.common.js');\nconst TerserPlugin = require('terser-webpack-plugin');\n\nmodule.exports = merge(common, {\n  mode: 'production',\n  optimization: {\n    minimize: true,\n    minimizer: [\n      new TerserPlugin({\n        terserOptions: {\n          keep_classnames: true,\n          keep_fnames: true,\n        },\n      }),\n    ],\n  },\n});\n"
  }
]