{
  "schemaVersion": "1.0",
  "item": {
    "slug": "line-client",
    "name": "Line Client",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/2manslkh/line-client",
    "canonicalUrl": "https://clawhub.ai/2manslkh/line-client",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/line-client",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=line-client",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/line-client"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/line-client",
    "agentPageUrl": "https://openagent3.xyz/skills/line-client/agent",
    "manifestUrl": "https://openagent3.xyz/skills/line-client/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/line-client/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "LINE Client Skill",
        "body": "Full LINE messaging client via the Chrome extension gateway JSON API."
      },
      {
        "title": "Repo & Files",
        "body": "Repo: /data/workspace/line-client (github.com/2manslkh/line-api)\nMain client: src/chrome_client.py → LineChromeClient\nQR login: src/auth/qr_login.py → QRLogin\nHMAC signer: src/hmac/signer.js (Node.js, auto-starts on port 18944)\nToken storage: ~/.line-client/tokens.json\nCertificate cache: ~/.line-client/sqr_cert\nWASM files: lstm.wasm + lstmSandbox.js (required, in repo root)"
      },
      {
        "title": "Quick Start",
        "body": "import json\nfrom pathlib import Path\nfrom src.chrome_client import LineChromeClient\n\ntokens = json.loads((Path.home() / \".line-client\" / \"tokens.json\").read_text())\nclient = LineChromeClient(auth_token=tokens[\"auth_token\"])\n\n# Send a message\nclient.send_message(\"U...\", \"Hello!\")\n\n# Get profile\nprofile = client.get_profile()\n\nTokens expire in ~7 days. If expired (APIError(10051)), re-run QR login."
      },
      {
        "title": "QR Login (Authentication)",
        "body": "QR login requires user interaction: scan QR on phone + enter PIN.\n\nfrom src.hmac import HmacSigner\nfrom src.auth.qr_login import QRLogin\nimport qrcode\n\nsigner = HmacSigner(mode=\"server\")\nlogin = QRLogin(signer)\nresult = login.run(\n    on_qr=lambda url: send_qr_image_to_user(qrcode.make(url)),\n    on_pin=lambda pin: send_pin_to_user_IMMEDIATELY(pin),  # TIME SENSITIVE!\n    on_status=lambda msg: print(msg),\n)\n# result.auth_token, result.mid, result.refresh_token\n\nCritical: The PIN must reach the user within ~60 seconds. Send it the instant on_pin fires."
      },
      {
        "title": "QR Login State Machine",
        "body": "createSession → session ID\ncreateQrCode → callback URL (append ?secret={curve25519_pubkey}&e2eeVersion=1)\ncheckQrCodeVerified — poll until scan (uses X-Line-Session-ID, no origin header)\nverifyCertificate — MUST be called even if it fails (required state transition!)\ncreatePinCode → 6-digit PIN (skip if cert verified in step 4)\ncheckPinCodeVerified — poll until user enters PIN\nqrCodeLoginV2 → JWT token + certificate + refresh token"
      },
      {
        "title": "Server-Side Login Script",
        "body": "python scripts/qr_login_server.py /tmp/qr.png\n\nEmits JSON events on stdout: {\"event\": \"qr\", \"path\": \"...\", \"url\": \"...\"}, {\"event\": \"pin\", \"pin\": \"123456\"}, {\"event\": \"done\", \"mid\": \"U...\"}."
      },
      {
        "title": "Contacts & Friends",
        "body": "MethodArgsDescriptionget_profile()—Get your own profile (displayName, mid, statusMessage, etc.)get_contact(mid)mid: strGet a single contact's profileget_contacts(mids)mids: list[str]Get multiple contactsget_all_contact_ids()—List all friend MIDsfind_contact_by_userid(userid)userid: strSearch by LINE IDfind_and_add_contact_by_mid(mid)mid: strAdd friend by MIDfind_contacts_by_phone(phones)phones: list[str]Search by phone numbersadd_friend_by_mid(mid)mid: strAdd friend (RelationService)get_blocked_contact_ids()—List blocked MIDsget_blocked_recommendation_ids()—List blocked recommendationsblock_contact(mid)mid: strBlock a contactunblock_contact(mid)mid: strUnblock a contactblock_recommendation(mid)mid: strBlock a friend suggestionupdate_contact_setting(mid, flag, value)mid, flag: int, value: strUpdate contact setting (e.g. mute)get_favorite_mids()—List favorited contact MIDsget_recommendation_ids()—List friend suggestions"
      },
      {
        "title": "Messages",
        "body": "MethodArgsDescriptionsend_message(to, text, ...)to: str, text: str, reply_to: str (opt)Send a text message. Supports replies via reply_to=message_idunsend_message(message_id)message_id: strUnsend/delete a sent messageget_recent_messages(chat_id, count=50)chat_id: strGet latest messages in a chatget_previous_messages(chat_id, end_seq, count=50)chat_id, end_seq: intPaginated history (older messages)get_messages_by_ids(message_ids)message_ids: list[str]Fetch specific messagesget_message_boxes(count=50)—Get chat list with last message (inbox view)get_message_boxes_by_ids(chat_ids)chat_ids: list[str]Get specific chats with last messageget_message_read_range(chat_ids)chat_ids: list[str]Get read receipt infosend_chat_checked(chat_id, last_message_id)chat_id, last_message_id: strMark messages as readsend_chat_removed(chat_id, last_message_id)chat_id, last_message_id: strRemove chat from inboxsend_postback(to, postback_data)to, postback_data: strSend postback (bot interactions)"
      },
      {
        "title": "Chats & Groups",
        "body": "MethodArgsDescriptionget_chats(chat_ids, with_members=True, with_invitees=True)chat_ids: list[str]Get chat/group detailsget_all_chat_mids()—List all chat MIDs (groups + invites)create_chat(name, target_mids)name: str, target_mids: list[str]Create a new group chataccept_chat_invitation(chat_id)chat_id: strAccept group invitereject_chat_invitation(chat_id)chat_id: strReject group inviteinvite_into_chat(chat_id, mids)chat_id: str, mids: list[str]Invite users to groupcancel_chat_invitation(chat_id, mids)chat_id: str, mids: list[str]Cancel pending invitesdelete_other_from_chat(chat_id, mids)chat_id: str, mids: list[str]Kick members from groupleave_chat(chat_id)chat_id: strLeave a group chatupdate_chat(chat_id, updates)chat_id: str, updates: dictUpdate group name/settingsset_chat_hidden_status(chat_id, hidden)chat_id: str, hidden: boolArchive/unarchive a chatget_rooms(room_ids)room_ids: list[str]Get legacy room infoinvite_into_room(room_id, mids)room_id: str, mids: list[str]Invite to legacy roomleave_room(room_id)room_id: strLeave legacy room"
      },
      {
        "title": "Reactions",
        "body": "MethodArgsDescriptionreact(message_id, reaction_type)message_id: str, type: intReact to a message. Types: 2=like, 3=love, 4=laugh, 5=surprised, 6=sad, 7=angrycancel_reaction(message_id)message_id: strRemove your reaction"
      },
      {
        "title": "Profile & Settings",
        "body": "MethodArgsDescriptionupdate_profile_attributes(attr, value, meta={})attr: int, value: strUpdate profile. Attrs: 2=DISPLAY_NAME, 16=STATUS_MESSAGE, 4=PICTURE_STATUSupdate_status_message(message)message: strShortcut: update status messageupdate_display_name(name)name: strShortcut: update display nameget_settings()—Get all account settingsget_settings_attributes(attr_bitset)attr_bitset: intGet specific settingsupdate_settings_attributes(attr_bitset, settings)attr_bitset: int, settings: dictUpdate settings"
      },
      {
        "title": "Polling & Events",
        "body": "MethodArgsDescriptionget_last_op_revision()—Get latest operation revision numberfetch_ops(count=50)—Fetch pending operations (may long-poll)poll()—Generator yielding operations as they arriveon_message(handler)handler: Callable(msg, client)Start polling thread, calls handler on new messages. Op types: 26=SEND_MESSAGE, 27=RECEIVE_MESSAGEstop()—Stop the polling thread"
      },
      {
        "title": "Other Services",
        "body": "MethodArgsDescriptionget_server_time()—Get LINE server timestampget_configurations()—Get server configurationsget_rsa_key_info()—Get RSA key for authissue_channel_token(channel_id)channel_id: strIssue channel token (LINE Login/LIFF)get_buddy_detail(mid)mid: strGet official account inforeport_abuse(mid, category=0, reason=\"\")mid: strReport a useradd_friend_by_mid(mid)mid: strAdd friend (RelationService)logout()—Logout and invalidate token"
      },
      {
        "title": "MID Format",
        "body": "LINE identifies entities by MID:\n\nU... or u... → User (toType=0)\nC... or c... → Group chat (toType=2)\nR... or r... → Room (toType=1)\n\nThe client auto-detects toType from the MID prefix when sending messages."
      },
      {
        "title": "HMAC Signing",
        "body": "All API calls require X-Hmac header. The WASM signer handles this automatically:\n\nDerives key from version \"3.7.1\" + access token via proprietary KDF (in lstm.wasm)\nSigns path + body → base64 → X-Hmac\nServer mode: ~13ms/sign (Node.js HTTP server on port 18944, auto-started)\nSubprocess mode: ~2s/sign (fallback)"
      },
      {
        "title": "Error Handling",
        "body": "from src.chrome_client import APIError\n\ntry:\n    client.send_message(mid, \"test\")\nexcept APIError as e:\n    print(e.code, e.api_message)\n    # 10051 = session expired / invalid\n    # 10052 = HTTP error from backend\n    # 10102 = invalid arguments"
      },
      {
        "title": "Architecture",
        "body": "User's Phone (LINE app)\n    ↕ (scan QR / enter PIN)\nLINE Servers (line-chrome-gw.line-apps.com)\n    ↕ (JSON REST + X-Hmac signing)\nLineChromeClient (this repo)\n    ↕ (WASM HMAC via Node.js signer)\nlstm.wasm + lstmSandbox.js\n\nThe Chrome Gateway translates JSON ↔ Thrift internally. We never deal with Thrift binary — everything is clean JSON."
      }
    ],
    "body": "LINE Client Skill\n\nFull LINE messaging client via the Chrome extension gateway JSON API.\n\nRepo & Files\nRepo: /data/workspace/line-client (github.com/2manslkh/line-api)\nMain client: src/chrome_client.py → LineChromeClient\nQR login: src/auth/qr_login.py → QRLogin\nHMAC signer: src/hmac/signer.js (Node.js, auto-starts on port 18944)\nToken storage: ~/.line-client/tokens.json\nCertificate cache: ~/.line-client/sqr_cert\nWASM files: lstm.wasm + lstmSandbox.js (required, in repo root)\nQuick Start\nimport json\nfrom pathlib import Path\nfrom src.chrome_client import LineChromeClient\n\ntokens = json.loads((Path.home() / \".line-client\" / \"tokens.json\").read_text())\nclient = LineChromeClient(auth_token=tokens[\"auth_token\"])\n\n# Send a message\nclient.send_message(\"U...\", \"Hello!\")\n\n# Get profile\nprofile = client.get_profile()\n\n\nTokens expire in ~7 days. If expired (APIError(10051)), re-run QR login.\n\nQR Login (Authentication)\n\nQR login requires user interaction: scan QR on phone + enter PIN.\n\nfrom src.hmac import HmacSigner\nfrom src.auth.qr_login import QRLogin\nimport qrcode\n\nsigner = HmacSigner(mode=\"server\")\nlogin = QRLogin(signer)\nresult = login.run(\n    on_qr=lambda url: send_qr_image_to_user(qrcode.make(url)),\n    on_pin=lambda pin: send_pin_to_user_IMMEDIATELY(pin),  # TIME SENSITIVE!\n    on_status=lambda msg: print(msg),\n)\n# result.auth_token, result.mid, result.refresh_token\n\n\nCritical: The PIN must reach the user within ~60 seconds. Send it the instant on_pin fires.\n\nQR Login State Machine\ncreateSession → session ID\ncreateQrCode → callback URL (append ?secret={curve25519_pubkey}&e2eeVersion=1)\ncheckQrCodeVerified — poll until scan (uses X-Line-Session-ID, no origin header)\nverifyCertificate — MUST be called even if it fails (required state transition!)\ncreatePinCode → 6-digit PIN (skip if cert verified in step 4)\ncheckPinCodeVerified — poll until user enters PIN\nqrCodeLoginV2 → JWT token + certificate + refresh token\nServer-Side Login Script\npython scripts/qr_login_server.py /tmp/qr.png\n\n\nEmits JSON events on stdout: {\"event\": \"qr\", \"path\": \"...\", \"url\": \"...\"}, {\"event\": \"pin\", \"pin\": \"123456\"}, {\"event\": \"done\", \"mid\": \"U...\"}.\n\nAll API Methods\nContacts & Friends\nMethod\tArgs\tDescription\nget_profile()\t—\tGet your own profile (displayName, mid, statusMessage, etc.)\nget_contact(mid)\tmid: str\tGet a single contact's profile\nget_contacts(mids)\tmids: list[str]\tGet multiple contacts\nget_all_contact_ids()\t—\tList all friend MIDs\nfind_contact_by_userid(userid)\tuserid: str\tSearch by LINE ID\nfind_and_add_contact_by_mid(mid)\tmid: str\tAdd friend by MID\nfind_contacts_by_phone(phones)\tphones: list[str]\tSearch by phone numbers\nadd_friend_by_mid(mid)\tmid: str\tAdd friend (RelationService)\nget_blocked_contact_ids()\t—\tList blocked MIDs\nget_blocked_recommendation_ids()\t—\tList blocked recommendations\nblock_contact(mid)\tmid: str\tBlock a contact\nunblock_contact(mid)\tmid: str\tUnblock a contact\nblock_recommendation(mid)\tmid: str\tBlock a friend suggestion\nupdate_contact_setting(mid, flag, value)\tmid, flag: int, value: str\tUpdate contact setting (e.g. mute)\nget_favorite_mids()\t—\tList favorited contact MIDs\nget_recommendation_ids()\t—\tList friend suggestions\nMessages\nMethod\tArgs\tDescription\nsend_message(to, text, ...)\tto: str, text: str, reply_to: str (opt)\tSend a text message. Supports replies via reply_to=message_id\nunsend_message(message_id)\tmessage_id: str\tUnsend/delete a sent message\nget_recent_messages(chat_id, count=50)\tchat_id: str\tGet latest messages in a chat\nget_previous_messages(chat_id, end_seq, count=50)\tchat_id, end_seq: int\tPaginated history (older messages)\nget_messages_by_ids(message_ids)\tmessage_ids: list[str]\tFetch specific messages\nget_message_boxes(count=50)\t—\tGet chat list with last message (inbox view)\nget_message_boxes_by_ids(chat_ids)\tchat_ids: list[str]\tGet specific chats with last message\nget_message_read_range(chat_ids)\tchat_ids: list[str]\tGet read receipt info\nsend_chat_checked(chat_id, last_message_id)\tchat_id, last_message_id: str\tMark messages as read\nsend_chat_removed(chat_id, last_message_id)\tchat_id, last_message_id: str\tRemove chat from inbox\nsend_postback(to, postback_data)\tto, postback_data: str\tSend postback (bot interactions)\nChats & Groups\nMethod\tArgs\tDescription\nget_chats(chat_ids, with_members=True, with_invitees=True)\tchat_ids: list[str]\tGet chat/group details\nget_all_chat_mids()\t—\tList all chat MIDs (groups + invites)\ncreate_chat(name, target_mids)\tname: str, target_mids: list[str]\tCreate a new group chat\naccept_chat_invitation(chat_id)\tchat_id: str\tAccept group invite\nreject_chat_invitation(chat_id)\tchat_id: str\tReject group invite\ninvite_into_chat(chat_id, mids)\tchat_id: str, mids: list[str]\tInvite users to group\ncancel_chat_invitation(chat_id, mids)\tchat_id: str, mids: list[str]\tCancel pending invites\ndelete_other_from_chat(chat_id, mids)\tchat_id: str, mids: list[str]\tKick members from group\nleave_chat(chat_id)\tchat_id: str\tLeave a group chat\nupdate_chat(chat_id, updates)\tchat_id: str, updates: dict\tUpdate group name/settings\nset_chat_hidden_status(chat_id, hidden)\tchat_id: str, hidden: bool\tArchive/unarchive a chat\nget_rooms(room_ids)\troom_ids: list[str]\tGet legacy room info\ninvite_into_room(room_id, mids)\troom_id: str, mids: list[str]\tInvite to legacy room\nleave_room(room_id)\troom_id: str\tLeave legacy room\nReactions\nMethod\tArgs\tDescription\nreact(message_id, reaction_type)\tmessage_id: str, type: int\tReact to a message. Types: 2=like, 3=love, 4=laugh, 5=surprised, 6=sad, 7=angry\ncancel_reaction(message_id)\tmessage_id: str\tRemove your reaction\nProfile & Settings\nMethod\tArgs\tDescription\nupdate_profile_attributes(attr, value, meta={})\tattr: int, value: str\tUpdate profile. Attrs: 2=DISPLAY_NAME, 16=STATUS_MESSAGE, 4=PICTURE_STATUS\nupdate_status_message(message)\tmessage: str\tShortcut: update status message\nupdate_display_name(name)\tname: str\tShortcut: update display name\nget_settings()\t—\tGet all account settings\nget_settings_attributes(attr_bitset)\tattr_bitset: int\tGet specific settings\nupdate_settings_attributes(attr_bitset, settings)\tattr_bitset: int, settings: dict\tUpdate settings\nPolling & Events\nMethod\tArgs\tDescription\nget_last_op_revision()\t—\tGet latest operation revision number\nfetch_ops(count=50)\t—\tFetch pending operations (may long-poll)\npoll()\t—\tGenerator yielding operations as they arrive\non_message(handler)\thandler: Callable(msg, client)\tStart polling thread, calls handler on new messages. Op types: 26=SEND_MESSAGE, 27=RECEIVE_MESSAGE\nstop()\t—\tStop the polling thread\nOther Services\nMethod\tArgs\tDescription\nget_server_time()\t—\tGet LINE server timestamp\nget_configurations()\t—\tGet server configurations\nget_rsa_key_info()\t—\tGet RSA key for auth\nissue_channel_token(channel_id)\tchannel_id: str\tIssue channel token (LINE Login/LIFF)\nget_buddy_detail(mid)\tmid: str\tGet official account info\nreport_abuse(mid, category=0, reason=\"\")\tmid: str\tReport a user\nadd_friend_by_mid(mid)\tmid: str\tAdd friend (RelationService)\nlogout()\t—\tLogout and invalidate token\nMID Format\n\nLINE identifies entities by MID:\n\nU... or u... → User (toType=0)\nC... or c... → Group chat (toType=2)\nR... or r... → Room (toType=1)\n\nThe client auto-detects toType from the MID prefix when sending messages.\n\nHMAC Signing\n\nAll API calls require X-Hmac header. The WASM signer handles this automatically:\n\nDerives key from version \"3.7.1\" + access token via proprietary KDF (in lstm.wasm)\nSigns path + body → base64 → X-Hmac\nServer mode: ~13ms/sign (Node.js HTTP server on port 18944, auto-started)\nSubprocess mode: ~2s/sign (fallback)\nError Handling\nfrom src.chrome_client import APIError\n\ntry:\n    client.send_message(mid, \"test\")\nexcept APIError as e:\n    print(e.code, e.api_message)\n    # 10051 = session expired / invalid\n    # 10052 = HTTP error from backend\n    # 10102 = invalid arguments\n\nArchitecture\nUser's Phone (LINE app)\n    ↕ (scan QR / enter PIN)\nLINE Servers (line-chrome-gw.line-apps.com)\n    ↕ (JSON REST + X-Hmac signing)\nLineChromeClient (this repo)\n    ↕ (WASM HMAC via Node.js signer)\nlstm.wasm + lstmSandbox.js\n\n\nThe Chrome Gateway translates JSON ↔ Thrift internally. We never deal with Thrift binary — everything is clean JSON."
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/2manslkh/line-client",
    "publisherUrl": "https://clawhub.ai/2manslkh/line-client",
    "owner": "2manslkh",
    "version": "1.0.0",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/line-client",
    "downloadUrl": "https://openagent3.xyz/downloads/line-client",
    "agentUrl": "https://openagent3.xyz/skills/line-client/agent",
    "manifestUrl": "https://openagent3.xyz/skills/line-client/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/line-client/agent.md"
  }
}