六角學院 - HexSchool
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Write
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Help
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Write
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # 第四堂:實務 AI 商業場景開發 ## AI 客服 (資料庫版本) * 由 [week4 版本調整](https://github.com/hexschool/node-training-postgresql) ### NPM 安裝 ```=json "dependencies": { "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "openai": "^4.9.2", "pg": "^8.11.3", "typeorm": "^0.3.17" }, ``` ### db.js ```=JavaScript // db.js const { DataSource, EntitySchema } = require("typeorm"); // 新增對話實體 const Conversation = new EntitySchema({ name: "Conversation", tableName: "CONVERSATION", columns: { id: { primary: true, type: "uuid", generated: "uuid", nullable: false, }, userId: { type: "varchar", length: 100, nullable: false, name: "user_id", }, lastActivity: { type: "timestamp", name: "last_activity", nullable: false, }, createdAt: { type: "timestamp", createDate: true, name: "created_at", nullable: false, }, }, }); // 新增消息實體 const Message = new EntitySchema({ name: "Message", tableName: "MESSAGE", columns: { id: { primary: true, type: "uuid", generated: "uuid", nullable: false, }, role: { type: "varchar", length: 20, nullable: false, }, content: { type: "text", nullable: false, }, timestamp: { type: "timestamp", nullable: false, }, }, relations: { conversation: { type: "many-to-one", target: "Conversation", joinColumn: { name: "conversation_id", }, onDelete: "CASCADE", }, }, }); const AppDataSource = new DataSource({ type: "postgres", host: process.env.DB_HOST || "localhost", port: process.env.DB_PORT || 5432, username: process.env.DB_USERNAME || "root", password: process.env.DB_PASSWORD || "test", database: process.env.DB_DATABASE || "test", entities: [Conversation, Message], synchronize: true, }); module.exports = AppDataSource; ``` ### server.js ```=javascript const express = require("express"); const OpenAI = require("openai"); const cors = require("cors"); require("dotenv").config(); const AppDataSource = require("./db"); const app = express(); const PORT = process.env.PORT || 3000; // 中間件 app.use(cors()); app.use(express.json()); app.use(express.static("public")); // 初始化 OpenAI const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); // 初始化資料庫連結 let dbInitialized = false; AppDataSource.initialize() .then(() => { console.log("資料庫連結成功"); dbInitialized = true; }) .catch((error) => { console.error("資料庫連結失敗:", error); }); // 處理客服查詢 app.post("/api/chat", async (req, res) => { try { if (!dbInitialized) { return res.status(503).json({ error: "資料庫尚未初始化,請稍後再試" }); } const { message, userId } = req.body; if (!message) { return res.status(400).json({ error: "缺少必要參數" }); } // 生成隨機用戶ID(如果沒有提供) const clientId = userId || `user_${Date.now()}`; // 取得對話實體 const conversationRepo = AppDataSource.getRepository("Conversation"); const messageRepo = AppDataSource.getRepository("Message"); // 尋找或建立對話 let conversation = await conversationRepo.findOne({ where: { userId: clientId }, }); if (!conversation) { conversation = conversationRepo.create({ userId: clientId, lastActivity: new Date(), }); await conversationRepo.save(conversation); } else { // 更新最後活動時間 conversation.lastActivity = new Date(); await conversationRepo.save(conversation); } // 新增用戶資訊到資料庫 await messageRepo.save( messageRepo.create({ conversation: conversation, role: "user", content: message, timestamp: new Date(), }) ); // 取得對話紀錄 const messageHistory = await messageRepo.find({ where: { conversation: { id: conversation.id } }, order: { timestamp: "ASC" }, }); // 將對話紀錄轉換為字串 const conversationHistory = messageHistory .map((msg) => `${msg.role === "user" ? "用戶" : "客服"}: ${msg.content}`) .join("\n"); // 構建系統提示 const stockInfo = `- 智慧型手機 X1:庫存 50 - 筆記型電腦 Pro:庫存 30 - 無線耳機:庫存 100 - 智慧型手錶:庫存 45 - 平板電腦:庫存 25`; const systemPrompt = `你是一個專業的客服機器人,專門負責回答商品庫存查詢。 目前商品庫存情況: ${stockInfo} 當用戶詢問商品庫存時,請提供上述最新資訊。如果用戶詢問其他商品,請告知我們目前沒有該商品的資訊。 如果用戶詢問非庫存相關問題,請禮貌地引導他們詢問商品庫存情況。`; // 呼叫 OpenAI Responses API 與 Function Calling const response = await openai.responses.create({ model: "gpt-4-turbo", instructions: systemPrompt, input: conversationHistory, tools: [ { type: "function", name: "provide_stock_info", description: "提供商品庫存資訊給用戶", parameters: { type: "object", properties: { response: { type: "string", description: "對用戶問題的回應", }, }, required: ["response"], }, }, ], tool_choice: { type: "function", name: "provide_stock_info" }, }); console.log(response); // 取得回應內容 const toolCall = response.output.find( (item) => item.type === "function_call" ); const responseData = JSON.parse(toolCall.arguments); // 增加 AI 客服回應到資料庫 await messageRepo.save( messageRepo.create({ conversation: conversation, role: "assistant", content: responseData.response, timestamp: new Date(), }) ); // 回傳結果 res.json({ response: responseData.response, userId: clientId, }); } catch (error) { console.error("處理客服查詢時出錯:", error); res.status(500).json({ error: "處理您的請求時發生錯誤", details: error.message, }); } }); // 啟動伺服器 app.listen(PORT, () => { console.log(`伺服器運行在 http://localhost:${PORT}`); }); ``` ### public/html ```=html <!DOCTYPE html> <html lang="zh-TW"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>客服機器人</title> <style> body { font-family: 'Microsoft JhengHei', Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; color: #333; } .container { max-width: 800px; margin: 20px auto; padding: 20px; background-color: #fff; border-radius: 10px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } h1 { text-align: center; color: #2c3e50; } .chat-container { height: 400px; overflow-y: auto; padding: 15px; border: 1px solid #ddd; border-radius: 5px; margin-bottom: 15px; } .message { margin-bottom: 10px; padding: 10px; border-radius: 5px; max-width: 80%; } .user-message { background-color: #dcf8c6; margin-left: auto; } .bot-message { background-color: #f1f0f0; } .input-container { display: flex; gap: 10px; } input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; } button { padding: 10px 15px; background-color: #2c3e50; color: white; border: none; border-radius: 5px; cursor: pointer; } button:hover { background-color: #1a252f; } .product-list { margin-top: 30px; } .product-list h2 { color: #2c3e50; border-bottom: 1px solid #ddd; padding-bottom: 10px; } .products { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; margin-top: 15px; } .product-card { border: 1px solid #ddd; border-radius: 5px; padding: 15px; background-color: #fff; } .product-name { font-weight: bold; margin-bottom: 5px; } .product-price { color: #e74c3c; font-weight: bold; } .product-stock { color: #27ae60; margin-top: 5px; } .loading { text-align: center; margin: 20px 0; color: #7f8c8d; } .stock-control { margin-top: 10px; display: flex; gap: 5px; } .stock-control input { width: 60px; } </style> </head> <body> <div class="container"> <h1>智能客服系統</h1> <div class="chat-container" id="chatContainer"> <div class="message bot-message"> 您好!我是您的客服助手。請問有什麼可以幫助您的嗎?您可以詢問商品庫存問題。 </div> </div> <div class="input-container"> <input type="text" id="userInput" placeholder="請輸入您的問題..." /> <button id="sendBtn">發送</button> <button id="clearBtn">清除對話</button> </div> </div> <script> let userId = localStorage.getItem('userId') || null; // 發送訊息到客服系統 async function sendMessage() { const userInput = document.getElementById('userInput'); const message = userInput.value.trim(); if (!message) return; // 加入用戶資訊到聊天介面 addMessage(message, 'user'); userInput.value = ''; // 顯示載入中 const loadingId = addMessage('正在思考...', 'bot'); try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message, userId, }), }); const data = await response.json(); // 移除載入訊息 removeMessage(loadingId); if (data.error) { addMessage(`發生錯誤: ${data.error}`, 'bot'); } else { // 保存用戶ID if (data.userId) { userId = data.userId; localStorage.setItem('userId', userId); } // 添加機器人回應 addMessage(data.response, 'bot'); } } catch (error) { console.error('發送訊息時出錯:', error); removeMessage(loadingId); addMessage('發送訊息時發生錯誤,請稍後再試。', 'bot'); } } // 加入訊息到聊天介面 function addMessage(text, sender) { const chatContainer = document.getElementById('chatContainer'); const messageElement = document.createElement('div'); messageElement.className = `message ${sender}-message`; messageElement.textContent = text; const messageId = Date.now(); messageElement.id = `msg-${messageId}`; chatContainer.appendChild(messageElement); chatContainer.scrollTop = chatContainer.scrollHeight; return messageId; } // 事件監聽器 document.getElementById('sendBtn').addEventListener('click', sendMessage); document.getElementById('clearBtn').addEventListener('click', clearConversation); document.getElementById('userInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') { sendMessage(); } }); </script> </body> </html> ``` ### fine-tuneing 流程 * [文件連結](https://platform.openai.com/docs/guides/fine-tuning) ### 步驟一:準備資料 ### data.jsonl 範例檔案 ```=jsonl {"messages":[{"role":"user","content":"如何使用智慧型手錶 X1?"},{"role":"assistant","content":"您可以先為手錶充電,並在手機上安裝專屬 App,打開藍牙進行配對後,即可開始量測步數、心率及睡眠品質。若需更多功能,請至設定頁面進行個人化調整。"}]} {"messages":[{"role":"user","content":"這款真皮皮包的保養方式是什麼?"},{"role":"assistant","content":"建議使用專業的皮革清潔劑及柔軟布料輕擦,切勿使用含酒精或刺激性成分的化學劑,以免破壞皮質。定期上油保養可延長皮革壽命,儲存時請放在乾燥通風處。"}]} {"messages":[{"role":"user","content":"請問該如何清潔筆電鍵盤?"},{"role":"assistant","content":"可先關機並斷電,再以微吸力吸塵器或壓縮空氣清理縫隙內的灰塵,切勿用水直接擦拭。若有油漬或指紋,請使用不含酒精的電子產品專用清潔濕巾。"}]} {"messages":[{"role":"user","content":"你們的運費怎麼計算?"},{"role":"assistant","content":"本店採用宅配到府,每筆訂單收取固定運費 80 元;若訂單金額滿 2000 元則享免運優惠。偏遠地區及離島運費依照物流公司規定另計。"}]} {"messages":[{"role":"user","content":"如果商品寄到後發現瑕疵,該怎麼辦?"},{"role":"assistant","content":"若您收到的商品有任何瑕疵或損壞,請先拍照留存並於 7 天內聯繫客服,客服人員會協助您申請退換貨並負擔回寄運費。"}]} {"messages":[{"role":"user","content":"要如何辦理退貨?"},{"role":"assistant","content":"在商品簽收後 7 天內未拆封、配件完整且保持原包裝的情況下,可申請退貨。請先聯繫客服取得退貨編號,再依照指示將商品寄回。我們會在收到並確認商品狀況後,於 5-7 個工作天內辦理退款。"}]} {"messages":[{"role":"user","content":"你們提供哪些付款方式?"},{"role":"assistant","content":"我們支援多種付款方式,包括信用卡、ATM 轉帳、行動支付(如 Apple Pay)和貨到付款。請在結帳頁面選擇最方便的方式即可。"}]} {"messages":[{"role":"user","content":"這家品牌怎麼創立的?"},{"role":"assistant","content":"本品牌於 2010 年由熱愛科技與創新設計的兩位大學生創立,初衷是將高品質產品與人性化介面結合,讓消費者能以平易近人的價格享受優質生活。經過多年發展,我們持續追求技術創新與品質提升。"}]} {"messages":[{"role":"user","content":"為什麼你們的商品要強調環保?"},{"role":"assistant","content":"我們相信永續是企業的社會責任,因此在原料選擇與生產過程中盡量採用環保材質,並透過再生能源與減少塑料包裝等實際行動,為環境盡一份力。"}]} {"messages":[{"role":"user","content":"若我的智慧型手錶 X1 壞掉了該怎麼辦?"},{"role":"assistant","content":"我們提供一年保固服務,若非人為因素造成的損壞可免費維修。請先聯繫客服並提供購買憑證、問題描述及照片,經檢測後若符合保固條件,會協助安排維修或更換新品。"}]} {"messages":[{"role":"user","content":"你們有提供海外運送嗎?"},{"role":"assistant","content":"目前我們只提供國內配送,尚未開放海外運送服務。若您有特殊需求,請直接聯繫客服,我們會視情況協助處理。"}]} {"messages":[{"role":"user","content":"購買後要怎麼保養無線耳機?"},{"role":"assistant","content":"建議在每次使用後,用柔軟的乾布或棉花棒擦拭耳機表面及充電盒;若接觸到汗水或水分,務必保持乾燥再進行充電。定期清潔耳塞部分可以維持音質與衛生。"}]} {"messages":[{"role":"user","content":"關於保固服務有什麼需要注意的地方?"},{"role":"assistant","content":"保固只適用於原廠硬體故障,人為損壞(如摔落、進水)或私自拆解則不在保固範圍。請於購買後保留發票或訂單證明,以便後續維修或更換。"}]} {"messages":[{"role":"user","content":"你們家產品最特別的地方是什麼?"},{"role":"assistant","content":"我們主打『質感生活、平易價格』,通過精心設計、嚴格品質控管與合理定價策略,讓更多人能以親民的價格享受高品質科技產品。"}]} {"messages":[{"role":"user","content":"運送大概需要多久時間?"},{"role":"assistant","content":"一般狀況下,週一至週五下午三點前完成訂單,當日即可出貨;出貨後約 1-3 個工作天送達。若遇連假或天候因素,運送時間可能會延誤。"}]} ``` > **注意** > - 每行是一個獨立的 JSON 物件。 > - 不要外加 `[]` 或在最後一行多逗號。 > - 每個物件裡都有 `"messages"` 屬性,且是一個陣列,陣列內包含至少一個 `{"role":"user"}` 與 `{"role":"assistant"}`。 > - 此格式才符合 GPT-4 最新 chat-based fine-tuning 的要求。 ### 步驟二:上傳檔案 (Files API) 1. **準備資料**(以 chat-based 格式) 2. **上傳資料檔案 (Files API)** 3. **啟動 Fine-tuning 工作 (Fine-tuning Jobs API)** 4. **檢查 Fine-tuning 狀態並使用自訂模型** 在 **Postman** 中: 1. 建立 **POST** 請求,URL 設為: ``` https://api.openai.com/v1/files ``` 2. **Headers** 設置: ``` Authorization: Bearer <YOUR_OPENAI_API_KEY> ``` > 另外可加: > - `Content-Type: multipart/form-data` > - 或直接在 Body 選擇 `form-data` 後,Postman 會自動帶入。 3. **Body** 選擇 `form-data`,新增兩個欄位: - `purpose`: `fine-tune` - `file`: (Type 選擇 File),上傳 jsonl 檔案 上傳成功後,會回傳一段 JSON,最重要的是 `id` 這個檔案編號: ```json { "id": "file-ABCDE12345", "object": "file", "bytes": 4567, "created_at": 1692190013, "filename": "training_data.jsonl", "purpose": "fine-tune", "status": "uploaded", "status_details": null } ``` --- ## 3. 建立 Fine-tuning 工作 (Fine-tuning Jobs API) 1. 接著在 **Postman** 新增 **POST** 請求,URL: ``` https://api.openai.com/v1/fine_tuning/jobs ``` 2. **Headers**: ``` Authorization: Bearer <YOUR_OPENAI_API_KEY> Content-Type: application/json ``` 3. **Body** (raw JSON): ```json { "training_file": "file-ABCDE12345", "model": "gpt-4", "n_epochs": 3 } ``` - `training_file`:填入上一步回傳的 `id`。 - `model`: 可觀看此 [model 列表](https://platform.openai.com/docs/guides/fine-tuning#token-limits) - `n_epochs`: 訓練的迭代次數,可自行調整。 送出後會回傳類似: ```json { "id": "ftjob-abcdef123456", "object": "fine_tuning.job", "model": "gpt-4", "created_at": 1692190113, "fine_tuned_model": null, "status": "running", "training_file": "file-ABCDE12345", ... } ``` 其中 `id` 就是這次 Fine-tuning 工作的隨機編號(例如 `ftjob-abcdef123456`)。 --- ## 4. 檢查 Fine-tuning 狀態 1. 建立 **GET** 請求: ``` https://api.openai.com/v1/fine_tuning/jobs/ftjob-abcdef123456 ``` 2. **Headers**: ``` Authorization: Bearer <YOUR_OPENAI_API_KEY> ``` 3. 送出後,若 `status` 為 `succeeded`,代表微調成功。 - 這時會看到一個 `fine_tuned_model` 屬性,例如: ``` "fine_tuned_model": "ft:gpt-4:org-1234:my-custom-model-2025-05-12-12-00-00" ``` - 這 ID 要記住,後續就能用來呼叫你專屬的模型。 --- ## 總結 1. **準備 chat-based JSONL**。 2. **上傳檔案**到 OpenAI Files API(purpose=fine-tune)。 3. **啟動 Fine-tuning** 工作(指定 `training_file`、`model`、`n_epochs` 等)。 4. **查詢狀態**直到 `status = succeeded`,並取得 `fine_tuned_model`。 5. **使用自訂模型**呼叫 ,將 `model` 指向微調後的 ID。 ### code ```=javascript const express = require("express"); const OpenAI = require("openai"); const cors = require("cors"); require("dotenv").config(); const AppDataSource = require("./db"); const app = express(); const PORT = process.env.PORT || 3000; // 中間件 app.use(cors()); app.use(express.json()); app.use(express.static("public")); // 初始化 OpenAI const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); // 初始化資料庫連結 let dbInitialized = false; AppDataSource.initialize() .then(() => { console.log("資料庫連結成功"); dbInitialized = true; }) .catch((error) => { console.error("資料庫連結失敗:", error); }); // 處理客服查詢 app.post("/api/chat", async (req, res) => { try { if (!dbInitialized) { return res.status(503).json({ error: "資料庫尚未初始化,請稍後再試" }); } const { message, userId } = req.body; if (!message) { return res.status(400).json({ error: "缺少必要參數" }); } // 生成隨機用戶ID(如果沒有提供) const clientId = userId || `user_${Date.now()}`; // 取得對話實體 const conversationRepo = AppDataSource.getRepository("Conversation"); const messageRepo = AppDataSource.getRepository("Message"); // 尋找或建立對話 let conversation = await conversationRepo.findOne({ where: { userId: clientId }, }); if (!conversation) { conversation = conversationRepo.create({ userId: clientId, lastActivity: new Date(), }); await conversationRepo.save(conversation); } else { // 更新最後活動時間 conversation.lastActivity = new Date(); await conversationRepo.save(conversation); } // 新增用戶資訊到資料庫 await messageRepo.save( messageRepo.create({ conversation: conversation, role: "user", content: message, timestamp: new Date(), }) ); // 取得對話紀錄 const messageHistory = await messageRepo.find({ where: { conversation: { id: conversation.id } }, order: { timestamp: "ASC" }, }); // 將對話紀錄轉換為字串 const conversationHistory = messageHistory .map((msg) => `${msg.role === "user" ? "用戶" : "客服"}: ${msg.content}`) .join("\n"); // 構建系統提示 const stockInfo = `- 智慧型手機 X1:庫存 50 - 筆記型電腦 Pro:庫存 30 - 無線耳機:庫存 100 - 智慧型手錶:庫存 45 - 平板電腦:庫存 25`; const systemPrompt = `你是一個專業的客服機器人,專門回答各種問題`; // 呼叫 OpenAI Responses API const response = await openai.responses.create({ model: "請填入自己的 model ID", instructions: systemPrompt, input: conversationHistory, }); // 取得回應內容 const responseText = response.output_text; // 增加 AI 客服回應到資料庫 await messageRepo.save( messageRepo.create({ conversation: conversation, role: "assistant", content: responseText, timestamp: new Date(), }) ); // 回傳結果 res.json({ response: responseText, userId: clientId, }); } catch (error) { console.error("處理客服查詢時出錯:", error); res.status(500).json({ error: "處理您的請求時發生錯誤", details: error.message, }); } }); // 啟動伺服器 app.listen(PORT, () => { console.log(`伺服器運行在 http://localhost:${PORT}`); }); ```

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully