Skills
16790 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.
Interact with the SalesBlink for cold email and sales outreach automation. Use when the user needs to send cold emails, manage email lists, sequences, templa...
--- name: cold-email-salesblink description: > Interact with the SalesBlink for cold email and sales outreach automation. Use when the user needs to send cold emails, manage email lists, sequences, templates, senders, leads, inbox replies, or campaign analytics via the SalesBlink API. Also use for bulk contact imports, workspace management, or deliverability testing via HTTP requests. compatibility: > Requires network access to run.salesblink.io and a SalesBlink API key. Supports any HTTP client (curl, Node.js fetch, Python requests, PowerShell, etc.). --- # SalesBlink Public REST API v1.0.0 ## When to use this skill Use this skill when the user wants to: - Create, update, or manage email lists, sequences, templates, or senders - Add, update, move, or remove contacts/leads - Send or reply to emails via the inbox - Check campaign analytics (opens, clicks, replies, sent) - Set up outreach campaigns end-to-end - Manage workspaces, users, folders, or deliverability tests - Make any HTTP request to `run.salesblink.io/api/public/v1.0.0` ## Gotchas - **ID types matter**: Templates and contact archive use MongoDB ObjectId (24-char hex). All other entities use UUID v4. - **messageId** is the RFC822 Message-ID (e.g. `<[email protected]>`) or Microsoft Graph ID. **Crucial:** Always URL-encode this ID when using it as a path parameter (e.g. in `/inbox/:messageId/thread`). This is distinct from the internal UUID `id`. - **`senders` is a comma-separated string**, not an array. It can mix sender IDs and folder IDs — the server auto-detects each. - **Sequence `steps` fully replace on PATCH**. Send the complete desired array. - **Verification flags are IRREVERSIBLE**: `verification`, `archive_invalid`, `archive_risky` on lists can only be turned ON, never OFF. - **Sequences default to paused**: If `paused` is omitted on create, it defaults to `true`. - **`launchTimingMode: "now"` starts in 5 minutes**, not instantly. - **Template attachments use FormData field `attachment`** (not `attachments`). Max 3 per template. - **Remove template attachments via `remove_attachments`** array of file **names**. - **Adding SMTP sender requires `from_email`**, not `email`. - **If an endpoint for a specific task is not mentioned then tell the user that the endpoint is not available** - **If user does not have a list, ask them for a CSV file, or list of lead emails with data.** - **If email sender is not connected, help them connect one using APIs.** - **When asked to create a sequence or campaign for cold email outreach, first ask them about their ICP, Offer, and other details.** ## Base URL `https://run.salesblink.io/api/public/v1.0.0` ## Authentication Ask the user for their SalesBlink API key: `https://run.salesblink.io/account/integration/api` Pass it in every request as the `Authorization` header (no "Bearer" prefix): **Header:** `Authorization: YOUR_API_KEY` ## Rate Limits | Method | Limit | Window | | ------------- | ----- | ---------- | | GET | 30 | per minute | | POST / PATCH | 15 | per minute | | PUT (archive) | 10 | per minute | On `429 Too Many Requests`: wait at least 60 seconds before retrying. For batch operations, insert a 4-second delay between requests. ## Pagination Most list endpoints use `limit` (max 100) and `skip`. Activity endpoints (`/sent`, `/opens`, `/clicks`, `/replies`) use `per_page` (max 100) and `page` (1-indexed). Always paginate. Never assume a single request returns all data. ## Endpoint Categories Read the relevant reference file before performing operations in that domain: - **Lists & contacts/leads** → [references/lists.md](references/lists.md) and [references/contacts.md](references/contacts.md) - Use these endpoints when the user wants to fetch or manage lists that contain leads/contacts. A list is a container for contacts/leads. Each contact/lead contains fields like Email, First_Name, Last_Name, Phone, Company, Title, and custom fields. Contacts are added to lists in batches (up to 500 per request), can be moved between lists, updated, or removed. - **Email templates** → [references/templates.md](references/templates.md) - Use these endpoints when the user wants to create or manage reusable email templates. A template has a name, subject_line, and HTML content that supports merge variables like {{first_name}} and {{company}}. Templates can have up to 3 attachments and are referenced by sequences when building outreach steps. - **Sequences & email campaigns** → [references/sequences.md](references/sequences.md) - Use these endpoints when the user wants to create or manage automated email campaigns (sequences). A sequence connects lists (who to email), senders (which accounts send), and templates (what to send) into a timed step-by-step workflow. Steps alternate between email sends and delay periods. Sequences can be launched, paused, resumed, cloned, or archived. - **Senders & OAuth** → [references/senders.md](references/senders.md) - Use these endpoints when the user wants to connect or manage email sending accounts. A sender is an email account (SMTP/IMAP or OAuth-connected Gmail/Outlook) that sends emails on behalf of sequences. Multiple senders can be assigned to a sequence. Senders can also be organized into folders. - **Inbox & replies** → [references/inbox.md](references/inbox.md) - Use these endpoints when the user wants to view or interact with email conversations. The inbox contains reply threads, sent emails, scheduled emails, and drafts. Each thread has a messageId. The user can reply to a lead's email, mark messages as read/unread, or classify outcomes. - **Activity tracking** → [references/activity.md](references/activity.md) - Use these endpoints when the user wants to query engagement events. The system tracks four event types: sent (emails sent), opens (emails opened), clicks (links clicked), and replies (responses received). Events can be filtered by sequence, recipient email, and date range. - **Users & workspaces** → [references/organization.md](references/organization.md) - Use these endpoints when the user wants to manage team membership or workspaces. A workspace is an account boundary. Users have roles (client, user, admin, developer). Only owners and admins can invite users or create workspaces. - **Folders** → [references/folders.md](references/folders.md) - Use these endpoints when the user wants to organize resources into folders. Folders have a type (list, template, sequence, or email-sender) and group related resources together for easier management. - **Domains, signatures & warmup links** → [references/account-config.md](references/account-config.md) - Use these endpoints when the user wants to view account-level configuration. Custom tracking domains are used for click tracking in emails. Signatures are appended to outgoing emails. Warmup links are used in email warmup processes. - **Reports** → [references/reports.md](references/reports.md) - Use these endpoints when the user wants to fetch aggregated activity reports over a date range. Reports combine data across campaigns into summary views. - **Inbox placement tests** → [references/inbox-placement.md](references/inbox-placement.md) - Use these endpoints when the user wants to test email deliverability. An inbox placement test sends a test email to seed email addresses across providers (Gmail, Outlook, etc.) and reports whether the email landed in inbox, spam, promotions, or other tabs. Tests can be one-time or recurring. - **End-to-end workflow examples** → [references/workflows.md](references/workflows.md) - Use this reference when the user wants to set up a complete outreach campaign from scratch. It shows the full chain: create list → add contacts → create templates → fetch senders → create sequence → launch. ## Error Handling Always check the `success` boolean in the response body. A `200` status can still return `{ success: false, message: "..." }`. | Status | Meaning | Action | | ------ | ------------ | ----------------------------------------------------- | | 200 | Success | Check `success` field | | 400 | Bad request | Re-check payload structure against the reference file | | 401 | Unauthorized | Verify API key | | 403 | Forbidden | Insufficient permissions (role too low) | | 404 | Not found | Verify the ID / endpoint | | 409 | Conflict | Resource already exists or connection failed | | 429 | Rate limited | Wait 60s, then retry | | 500 | Server error | Retry once after 10s | FILE:references/workflows.md # Workflow Examples ## End-to-End Campaign Setup Goal: Build a complete outreach campaign from scratch. ### Step 1 — Create a list **POST** `/lists` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Prospects", "removeDuplicates": { "inThisList": true, "inOtherLists": true } } ``` Extract `LIST_ID` from the response. ### Step 2 — Add leads to the list **POST** `/contacts` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "LIST_ID_HERE", "contacts": [ { "Email": "[email protected]", "First_Name": "Alice", "Company": "Corp Inc" }, { "Email": "[email protected]", "First_Name": "Bob", "Company": "Startup IO" } ], "remove_duplicates": true } ``` ### Step 3 — Create email templates **POST** `/templates` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Intro Email", "subject_line": "Quick question, {{first_name}}", "content": "<p>Hi {{first_name}},</p><p>I noticed {{company}} is scaling fast...</p>" } ``` Extract `TEMPLATE_1_ID` (MongoDB ObjectId). Repeat for follow-up templates. ### Step 4 — Fetch available senders **GET** `/senders` Headers: - `Authorization`: `YOUR_API_KEY` Extract target `SENDER_ID` (UUID). ### Step 5 — Create the sequence **POST** `/sequences` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Outbound Campaign", "senders": "SENDER_ID_HERE", "lists": ["LIST_ID_HERE"], "steps": [ { "type": "email", "template_id": "TEMPLATE_1_ID" }, { "type": "delay", "days": 3 }, { "type": "email", "template_id": "TEMPLATE_2_ID" } ], "paused": false, "launchTimingMode": "now" } ``` ## Clone and Modify Workflow Goal: Duplicate an existing sequence and tweak it. ### 1. Clone the sequence (creates paused copy) **POST** `/sequences/:id/clone` Headers: - `Authorization`: `YOUR_API_KEY` Extract `NEW_SEQ_ID` from response. ### 2. Update the cloned sequence with new settings **PATCH** `/sequences/:id` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Campaign — Variant B", "steps": [ { "type": "email", "template_id": "NEW_TEMPLATE_ID" }, { "type": "delay", "days": 5 }, { "type": "email", "template_id": "FOLLOW_UP_TEMPLATE_ID" } ] } ``` ### 3. Launch when ready **PATCH** `/sequences/:id` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "paused": false, "launchTimingMode": "now" } ``` ## Archive Cleanup Workflow Goal: Clean up old campaigns, lists, and templates. ### Archive a sequence (pauses it and removes pending tasks) **PUT** `/sequences/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` ### Archive the associated list **PUT** `/lists/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` ### Archive templates **PUT** `/templates/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` > Archiving a sequence automatically pauses it and removes pending email tasks. Archiving a list pauses it if active. Archiving a contact removes its pending tasks from all sequences. FILE:references/reports.md # Reports ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/reports` | GET | Retrieve activity reports with aggregated data | ## Get Reports **GET** `/reports` Headers: - `Authorization`: `YOUR_API_KEY` Query params: | Param | Type | Description | |-------|------|-------------| | `from` | integer | Start date timestamp (milliseconds) | | `to` | integer | End date timestamp (milliseconds) | | `limit` | integer | Max 100 (default enforced server-side) | | `skip` | integer | Offset | > The endpoint maps `from`/`to` to a date range filter internally. Both are optional; omitting them returns all available report data. **GET** `/reports?from=1715000000000&to=1717600000000` Headers: - `Authorization`: `YOUR_API_KEY` FILE:references/inbox.md # Inbox & Outreach ## Endpoints | Endpoint | Method | Description | | -------------------------- | ------ | ----------------------------------------------- | | `/inbox` | GET | Retrieve inbox threads | | `/inbox/:messageId/thread` | GET | Get all messages in a specific thread | | `/inbox/:messageId/reply` | POST | Reply to a lead's email | | `/inbox/:messageId` | PATCH | Mark as read/unread, set outcome classification | ## Get Inbox **GET** `/inbox` Headers: - `Authorization`: `YOUR_API_KEY` Query params: | Param | Type | Description | | ---------- | ------- | --------------------------------------------------------- | | `type` | string | `all` (replies, default), `draft`, `scheduled`, or `sent` | | `limit` | integer | Max 100 (default: 10) | | `skip` | integer | Offset (default: 0) | | `sequence` | string | Filter by sequence UUID | | `outcome` | string | Filter by outcome classification | | `search` | string | Search in body, subject, or email address | | `date` | string | Date range as `startTimestamp-endTimestamp` | | `sender` | string | Filter by sender ID | | `owned_by` | string | Filter by user email (owners/admins only) | ``` GET /inbox?type=all&limit=50&skip=0 GET /inbox?type=sent&sequence=SEQ_ID&limit=100 GET /inbox?search=acme&limit=20 ``` Response includes `data.result` (thread array), `totalCount`, `count`, and `messageIDs`. ## Get Thread **GET** `/inbox/:messageId/thread` Headers: - `Authorization`: `YOUR_API_KEY` Returns all messages in a conversation thread, sorted newest first. ## Reply to Email **POST** `/inbox/:messageId/reply` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "content": "<p>Thanks for getting back to me! Let's schedule a call.</p>", "cc": "[email protected]" } ``` | Field | Type | Req | Description | | ------------------ | ------- | --- | ------------------------------------------------------- | | `content` | string | ✅ | HTML content of the reply | | `cc` | string | | Optional CC email address | | `bcc` | string | | Optional BCC email address | | `scheduled_time` | integer | | Schedule at this timestamp (ms). Defaults to ~20s delay | | `tzMode` | string | | Timezone mode: `"sequence"` or `"custom"` | | `selectedTimezone` | string | | Timezone identifier if tzMode is custom | > Attachments are supported via FormData field `attachment`. Base64 images in HTML are automatically uploaded to S3. > The reply is automatically sent from the same sender that originally contacted the lead. ## Update Mail State **PATCH** `/inbox/:messageId` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "unread": false, "outcome": "interested" } ``` | Field | Type | Description | | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `unread` | boolean | Mark as read (`false`) or unread (`true`) | | `outcome` | string | Classify the reply: `"interested"`, `"not-interested"`, `"automatic-response"`, `"meeting-request"`, `"out-of-office"`, `"do-not-contact"`, `"wrong-person"`, `"closed"` | FILE:references/senders.md # Email Senders & OAuth ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/senders` | GET | List all connected senders (grouped by folder) | | `/senders` | POST | Add a single SMTP/IMAP sender | | `/senders/bulk` | POST | Bulk add senders via CSV upload | | `/oauth/google` | POST | Get Google OAuth URL for connecting Gmail | | `/oauth/outlook` | POST | Get Microsoft OAuth URL for connecting Outlook | ## Get Senders **GET** `/senders` Headers: - `Authorization`: `YOUR_API_KEY` Query params: `limit` (max 100), `skip`, `owned_by` ## Add Single Sender (SMTP/IMAP) **POST** `/senders` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "from_email": "[email protected]", "from_name": "Sales Team", "password": "your_password", "smtp_host": "smtp.yourprovider.com", "smtp_port": 587, "user_name": "[email protected]", "imap_host": "imap.yourprovider.com", "imap_port": 993 } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `from_email` | string | ✅ | Sender email address | | `password` | string | ✅ | SMTP/IMAP password | | `smtp_host` | string | ✅ | SMTP server hostname | | `smtp_port` | integer/string | ✅ | SMTP port (e.g., 587) | | `from_name` | string | | Display name | | `user_name` | string | | SMTP username (defaults to `from_email`) | | `imap_host` | string | | IMAP hostname (omit for SMTP-only) | | `imap_port` | integer/string | | IMAP port (e.g., 993) | | `imap_user_name` | string | | IMAP username if different from SMTP | | `imap_password` | string | | IMAP password if different from SMTP | | `total_warmup_per_day` | integer | | Warmup emails per day (default: 5) | | `warmup_enabled` | boolean | | Enable warmup (default: false) | | `inbox_enable` | boolean | | Enable inbox (default: false) | | `warmup_tag` | string | | Warmup keyword/tag | | `inbox_path` | string | | Inbox folder path (default: "INBOX") | | `spam_path` | string | | Spam folder path | | `signature_id` | string | | Signature ID or name to attach | | `custom_tracking_url` | string | | Custom tracking domain (must be verified) | | `sequence_auto_ramp_up_enabled` | boolean | | Enable sequence ramp-up | | `sequence_initial_daily_frequency` | integer | | Initial daily send limit (default: 30) | | `sequence_ramp_up_frequency` | integer | | Ramp-up increment (default: 3) | | `max_emails_per_day` | integer | | Max daily send limit (default: 30) | | `dkim_identifier` | string | | DKIM identifier | | `reply_to_email` | string | | Reply-to email address | > If `imap_host` is omitted or empty, the sender is created as **SMTP-only** (`serviceName: "smtponly"`). ## Bulk Add Senders **POST** `/senders/bulk` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `multipart/form-data` Upload a CSV file via FormData with field name `csvFile`. ## Google OAuth **POST** `/oauth/google` Headers: - `Authorization`: `YOUR_API_KEY` Returns an `auth_url` that the user must visit to authorize Gmail access. Response: ```json { "success": true, "data": { "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?..." } } ``` ## Outlook OAuth **POST** `/oauth/outlook` Headers: - `Authorization`: `YOUR_API_KEY` Returns an `auth_url` for Microsoft Outlook authorization. FILE:references/templates.md # Email Templates ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/templates` | GET | List all templates (includes `cold_email_score`) | | `/templates/:id` | GET | Get full template details + score breakdown | | `/templates` | POST | Create a template | | `/templates/:id` | PATCH | Update template content, attachments | | `/templates/:id/archive` | PUT | Archive or unarchive a template | ## Create Template **POST** `/templates` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Cold Outreach V1", "subject_line": "Quick question, {{first_name}}", "content": "<p>Hi {{first_name}},</p><p>I noticed {{company}} is scaling fast...</p>", "folder": "folder_id", "starred": false } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Template name | | `subject_line` | string | ✅ | Email subject (supports `{{variables}}`) | | `content` | string | ✅ | HTML body (supports `{{first_name}}`, `{{company}}`, etc.) | | `folder` | string | | Folder ID (UUID) | | `starred` | boolean | | Star the template | | `attachments` | file[] | | Files to attach (**max 3 total**). Use FormData field name `attachment` | Response includes `cold_email_score`: ```json { "score": 72.5, "rating": "Good", "details": { "word_count": 85, "personalization_count": 3, "link_count": 1, "image_count": 0, "question_count": 1, "spam_word_count": 0 } } ``` ## Update Template **PATCH** `/templates/:id` (ObjectId) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Updated Template", "subject_line": "New subject {{first_name}}", "content": "<p>Updated content here</p>", "remove_attachments": ["old_file.pdf"] } ``` | Field | Type | Description | |-------|------|-------------| | `name` | string | New template name | | `subject_line` | string | New subject line | | `content` | string | New HTML body | | `starred` | boolean | Star/unstar | | `attachments` | file[] | New files to **append** (total must not exceed 3). Use FormData field name `attachment` | | `remove_attachments` | string[] | Names of existing attachments to **remove** | > **IMPORTANT**: To remove attachments, use the `remove_attachments` array with file **names** — not the `attachments` field. ## Archive Template **PUT** `/templates/:id/archive` (ObjectId) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` FILE:references/inbox-placement.md # Inbox Placement Tests ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/inbox-placement` | GET | List deliverability tests | | `/inbox-placement` | POST | Create a new deliverability test | | `/inbox-placement/:id/pause` | PUT | Pause an active **recurring** test | | `/inbox-placement/:id` | DELETE | Delete a test | ## Get Tests **GET** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` Query params: | Param | Type | Description | |-------|------|-------------| | `search` | string | Filter by test name (case-insensitive) | | `status` | string | Filter by status: `pending`, `running`, `completed`, `stopped` | | `mode` | string | Filter by mode: `one-time`, `recurring` | | `ownedBy` | string | Filter by user ID | | `limit` | integer | Page size (default: 10) | | `skip` | integer | Offset (default: 0) | | `sortBy` | string | Sort field (default: `created_at`) | | `sortType` | integer | `-1` for descending, `1` for ascending | > Note: filtering by `status=completed` excludes recurring tests since they never truly complete. ## Create Test **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Test name (min 3 characters) | | `mode` | string | ✅ | `"one-time"` or `"recurring"` | | `source` | string | ✅ | `"from-salesblink"` or `"from-outside"` | | `sender_id` | string | ✅* | Sender UUID (required when `source="from-salesblink"` or `mode="recurring"`) | | `content_type` | string | ✅* | `"custom"`, `"sequence"`, or `"template"` (required when `source="from-salesblink"` or `mode="recurring"`) | | `subject` | string | ✅* | Email subject (required when `content_type="custom"`) | | `body` | string | ✅* | Email HTML body (required when `content_type="custom"`) | | `sequence_id` | string | ✅* | Sequence UUID (required when `content_type="sequence"`) | | `template_id` | string | ✅* | Template ObjectId (required when `content_type="sequence"` or `"template"`) | | `schedule_day` | integer | ✅* | Day of week: `0`=Sunday through `6`=Saturday (required when `mode="recurring"`) | | `tracking_uuid` | string | | Optional UUID for `from-outside` tests. Auto-generated if omitted. | > *Field requirement depends on `source`, `mode`, and `content_type` values. **Behavior:** - **One-time tests** run ~2 minutes after creation by default. - **Recurring tests** run weekly on the specified `schedule_day` at 09:00 UTC. - **`from-outside` tests** return `seed_emails` in the response — these are the addresses the user must send to. - **`from-salesblink` tests** send automatically using the selected sender. **V1 Wrapper behavior:** When `source="from-salesblink"` is provided without `content_type`: - If `subject` and `body` are present → `content_type` becomes `"custom"` - Otherwise → `content_type` becomes `"sequence"` with `sequence_id: "from_api"` ### Example: One-time custom content test **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Gmail Deliverability Check", "mode": "one-time", "source": "from-salesblink", "sender_id": "sender-uuid-here", "content_type": "custom", "subject": "Hello from SalesBlink", "body": "<p>This is a test email.</p>" } ``` ### Example: Recurring sequence-based test **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Weekly Sequence Check", "mode": "recurring", "source": "from-salesblink", "sender_id": "sender-uuid-here", "content_type": "sequence", "sequence_id": "sequence-uuid-here", "template_id": "507f1f77bcf86cd799439011", "schedule_day": 1 } ``` ### Example: From-outside test (returns seed emails) **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "External Send Test", "mode": "one-time", "source": "from-outside" } ``` Response for `from-outside`: ```json { "success": true, "data": { "id": "...", "tracking_uuid": "...", ... }, "seed_emails": ["[email protected]", "[email protected]", ...] } ``` ## Pause Test **PUT** `/inbox-placement/:id/pause` Headers: - `Authorization`: `YOUR_API_KEY` Only works on **recurring** tests. Sets status to `stopped` and clears scheduling. ## Delete Test **DELETE** `/inbox-placement/:id` Headers: - `Authorization`: `YOUR_API_KEY` Deletes the test and all associated tracking tasks. FILE:references/folders.md # Folders ## Endpoints | Endpoint | Method | Description | | ---------- | ------ | --------------- | | `/folders` | GET | List folders | | `/folders` | POST | Create a folder | ## Get Folders **GET** `/folders` Headers: - `Authorization`: `YOUR_API_KEY` ## Create Folder **POST** `/folders` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Campaigns", "type": "sequence" } ``` | Field | Type | Req | Description | | ------ | ------ | --- | --------------------------------------------------------- | | `name` | string | ✅ | Folder name | | `type` | string | ✅ | `"list"`, `"template"`, `"sequence"`, or `"email-sender"` | > If `type` contains `"sender"`, it is automatically converted to `"email-sender"`. FILE:references/contacts.md # Contacts & Leads ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/lists/:id/leads` | GET | Get leads in a list (paginated) | | `/contacts` | POST | Add up to 500 leads to a list | | `/contacts/remove` | POST | Remove a single lead by email from a list | | `/leads/:id` | PATCH | Update lead fields | | `/leads/:id/move` | PUT | Move a lead to a different list | | `/contacts/:id/archive` | PUT | Archive or unarchive a contact | ## Get Leads **GET** `/lists/:id/leads?limit=100&skip=0` Headers: - `Authorization`: `YOUR_API_KEY` Query params: `limit` (max 100), `skip` ## Add Contacts **POST** `/contacts` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "a1b2c3d4-e5f6-7890-abcd-abcdef123456", "contacts": [ { "Email": "[email protected]", "First_Name": "John", "Last_Name": "Doe", "Phone": "+1234567890", "Company": "Acme Inc", "Title": "VP Sales", "Custom_Field": "any value" } ], "remove_duplicates": true } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `list_id` | string | ✅ | List UUID to add leads to | | `contacts` | object[] | ✅ | Array of lead objects (**max 500 per request**) | | `remove_duplicates` | boolean | | Remove duplicate emails after insert | Each contact object: | Field | Type | Req | Description | |-------|------|-----|-------------| | `Email` | string | ✅ | Lead's email address | | `First_Name` | string | | First name | | `Last_Name` | string | | Last name | | `Phone` | string | | Phone number | | `Company` | string | | Company name | | `Title` | string | | Job title | | _(any key)_ | string | | Custom fields are supported | > **Field naming**: Use **PascalCase with underscores** (`First_Name`, `Last_Name`, `Email`). ## Remove Contact **POST** `/contacts/remove` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "a1b2c3d4-e5f6-7890-abcd-abcdef123456", "email": "[email protected]" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `list_id` | string | ✅ | List UUID | | `email` | string | ✅ | Email address of the lead to remove | ## Update Lead **PATCH** `/leads/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "First_Name": "Updated", "Last_Name": "Name", "Title": "CTO" } ``` Any standard or custom contact fields can be updated. System fields (`_id`, `id`, `list_id`, `account_id`, `user_id`, `accuracy`, `provider`, `custom_fields`, `removed_sequences`, `verification_required`, `archive_invalid_contacts`, `archive_risky_contacts`, `processing`, `completed`, `completedAt`, `last_modified`, `created_date`, `verification_blocked`, `didOpen`, `didClick`, `didReply`, `contactStats`, `retryCount`, `esg_name`, `archived`, `deleted`) **cannot** be modified. If updating `Email`, it is automatically lowercased. ## Move Lead **PUT** `/leads/:id/move` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "destination_list_uuid" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `list_id` | string | ✅ | Destination list UUID | ## Archive Contact **PUT** `/contacts/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` > ⚠️ **The `:id` here is a MongoDB ObjectId** (24-char hex), NOT a UUID. This is the only contact endpoint that uses ObjectId. FILE:references/lists.md # Lists ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/lists` | GET | Retrieve all lists. Query: `limit` (max 100), `skip`, `owned_by` | | `/lists/:id` | GET | Get a specific list by UUID | | `/lists/:id/leads` | GET | Get leads in a list. Query: `limit` (max 100), `skip` | | `/lists` | POST | Create a new list | | `/lists/:id` | PATCH | Update a list | | `/lists/:id/archive` | PUT | Archive or unarchive a list | ## Create List **POST** `/lists` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Prospects", "removeDuplicates": { "inThisList": true, "inOtherLists": true } } ``` Required fields marked with ✅: | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | List name | | `folder` | string | | Folder ID (UUID) | | `starred` | boolean | | Star the list (default: false) | | `verification` | boolean | | Enable email verification ⚠️ **IRREVERSIBLE** | | `archive_invalid` | boolean | | Auto-archive invalid emails ⚠️ **IRREVERSIBLE** | | `archive_risky` | boolean | | Auto-archive risky emails ⚠️ **IRREVERSIBLE** | | `removeDuplicates.inThisList` | boolean | | Remove duplicate emails within this list | | `removeDuplicates.inOtherLists` | boolean | | Remove contacts that exist in other lists | | `removeDuplicates.inTeamMembersLists` | boolean | | Remove contacts that exist in team members' lists | ## Update List **PATCH** `/lists/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q2 Prospects Restructured", "starred": true, "archive_invalid": true } ``` | Field | Type | Description | |-------|------|-------------| | `name` | string | New name | | `starred` | boolean | Star or unstar | | `duplicate_removal` | boolean | Remove duplicates from this list | | `duplicate_removal_other_list` | boolean | Remove contacts in other lists | | `duplicate_removal_team_list` | boolean | Remove contacts in team members' lists | | `verification` | boolean | Enable verification ⚠️ **IRREVERSIBLE** | | `archive_invalid` | boolean | Archive invalid emails ⚠️ **IRREVERSIBLE** | | `archive_risky` | boolean | Archive risky emails ⚠️ **IRREVERSIBLE** | > Use `PUT /lists/:id/archive` for archiving — not this endpoint. ## Archive List **PUT** `/lists/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` Set `"archived": false` to unarchive. FILE:references/sequences.md # Sequences ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/sequences` | GET | List all sequences. Query: `limit`, `skip`, `owned_by` | | `/sequences/:id` | GET | Get sequence details including steps and settings | | `/sequences/:id/stats` | GET | Get performance analytics. Query: `from`, `to`, `sender` | | `/sequences` | POST | Create a full sequence with steps, senders, lists | | `/sequences/:id` | PATCH | Update settings, pause/resume, rewrite steps | | `/sequences/:id/clone` | POST | Duplicate an existing sequence (created paused) | | `/sequences/:id/archive` | PUT | Archive or unarchive a sequence | ## Create Sequence **POST** `/sequences` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Outbound Campaign", "senders": "a1b2c3d4-e5f6-7890-abcd-000000000001,a1b2c3d4-e5f6-7890-abcd-000000000002", "lists": ["b2c3d4e5-f6a7-8901-bcde-000000000001"], "steps": [ { "type": "email", "template_id": "507f1f77bcf86cd799439011" }, { "type": "delay", "days": 3 }, { "type": "email", "template_id": "507f1f77bcf86cd799439012" } ], "paused": false, "launchTimingMode": "now", "timezone": "America/New_York", "delayEnabled": true, "delayFrom": 10, "delayTo": 20, "stopWhenReplyRecieved": true, "stopWhenReplyRecievedWhen": "contact" } ``` Required fields marked with ✅: | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Sequence name | | `senders` | string | ✅ | **Comma-separated string** of sender/folder IDs (NOT an array) | | `lists` | string[] | ✅ | Array of list UUIDs | | `steps` | Step[] | ✅ | Ordered array of email and delay steps | | `folder` | string | | Folder ID (UUID) | | `starred` | boolean | | Star the sequence | | `paused` | boolean | | Create in paused state (default: **true**) | | `launchTimingMode` | string | | `"now"` (starts in 5 mins) or `"schedule"` (requires `scheduledAt`) | | `scheduledAt` | integer | | UTC timestamp in **milliseconds** (required if mode = `"schedule"`) | | `timezone` | string | | Timezone for sending (default: `"America/New_York"`) | | `delayEnabled` | boolean | | Enable random delay between emails (default: true) | | `delayFrom` | integer | | Minimum delay in minutes (default: 10) | | `delayTo` | integer | | Maximum delay in minutes (default: 20) | | `stopWhenReplyRecieved` | boolean | | Stop sequence when lead replies (default: true) | | `stopWhenReplyRecievedWhen` | string | | `"contact"` or `"contact-with-same-domain"` (default: `"contact"`) | | `evergreen` | boolean | | Enable evergreen mode — continuously running (default: false) | | `bounceThreshold` | integer | | Bounces before pausing (default: 2) | | `bouncePause` | boolean | | Pause sequence on bounce threshold (default: false) | | `autoPause` | boolean | | Auto-pause on high bounce rate (default: true) | | `autoTagReplies` | boolean | | Auto-tag reply outcomes (default: false) | | `plainText` | boolean | | Send as plain text email (default: true) | | `auto_reply` | boolean | | Enable auto-reply detection (default: true) | | `matchProvider` | boolean | | Match sender email provider with recipient (default: true) | | `skip_esg` | boolean | | Skip ESG detection (default: true) | | `sendToOnlyVerifiedEmail` | boolean | | Only send to verified emails (default: false) | | `validEmail` | boolean | | Send to contacts with valid email status (default: true) | | `riskyEmail` | boolean | | Send to contacts with risky email status (default: true) | | `invalidEmail` | boolean | | Send to contacts with invalid email status (default: true) | | `checkEmailOpen` | boolean | | Check if recipient opened previous email before sending next (default: false) | | `checkEmailClick` | boolean | | Check if recipient clicked link before sending next (default: false) | | `checkEmailReply` | boolean | | Check if recipient replied before sending next (default: true) | | `checkEmailBeforeSending` | boolean | | Verify email before sending (default: true) | | `bcc` | string | | BCC email address for all outgoing emails (default: `""`) | | `emailSendingHours` | array | | Sending hours per day of the week (see default below) | **Steps structure** — ordered array mixing `email` and `delay` types: ```json "steps": [ { "type": "email", "template_id": "507f1f77bcf86cd799439011" }, { "type": "delay", "days": 3 }, { "type": "email", "template_id": "507f1f77bcf86cd799439012" } ] ``` - `type: "email"` → MUST include `template_id` (the template's MongoDB ObjectId). - `type: "delay"` → MUST include `days` (integer, number of days to wait). - Omitting `type` or misspelling it will fail or create a broken sequence. **Default `emailSendingHours`:** ```json [ { "enabled": true, "name": "Monday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Tuesday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Wednesday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Thursday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Friday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": false, "name": "Saturday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": false, "name": "Sunday", "fromTime": "09:00", "toTime": "17:00" } ] ``` ## Update Sequence **PATCH** `/sequences/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "paused": true } ``` Accepts the same fields as create (all optional). Additionally used to **pause/resume**. > **Critical**: When updating `steps`, the entire array is **replaced** — send the full desired step list. > Updates to `name`, `starred`, or `folder` do **not** trigger task rescheduling. All other field updates do. ## Clone Sequence **POST** `/sequences/:id/clone` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` Creates a paused duplicate of an existing sequence. ## Archive Sequence **PUT** `/sequences/:id/archive` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` Archiving a sequence automatically pauses it and removes pending email tasks. FILE:references/activity.md # Activity Tracking ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/sent` | GET | Log of all sent emails | | `/opens` | GET | Email open events | | `/clicks` | GET | Link click events | | `/replies` | GET | Reply events | ## Query Parameters All activity endpoints support: | Param | Type | Description | |-------|------|-------------| | `per_page` | integer | Max 100 | | `page` | integer | 1-indexed | | `sequence_id` | string | Filter by sequence UUID | | `recipient_email_address` | string | Filter by email address | | `since` | integer | Filter events after this timestamp (ms) | | `from` | integer | Start of date range (timestamp, ms) | | `to` | integer | End of date range (timestamp, ms) | > Use `per_page` and `page` for activity endpoints — not `limit`/`skip`. ## Response Format Each event includes: ```json { "id": "...", "time": 1715000000000, "message": "Sent", "type": "outreach", "sequence": "sequence-uuid", "email": "[email protected]", "sequence_name": "Campaign Name" } ``` For clicks and replies, `template_name` is also included. ## Examples **GET** `/opens?sequence_id=SEQ_ID&per_page=100&page=1` Headers: - `Authorization`: `YOUR_API_KEY` **GET** `/replies?since=TIMESTAMP_30_DAYS_AGO&per_page=100` Headers: - `Authorization`: `YOUR_API_KEY` FILE:references/account-config.md # Account Config — Domains, Signatures & Warmup Links ## Domains **GET** `/domains` Headers: - `Authorization`: `YOUR_API_KEY` List custom tracking domains for the account. ## Signatures **GET** `/signatures` Headers: - `Authorization`: `YOUR_API_KEY` List email signatures. > Signature IDs can be referenced when adding senders via the `signature_id` field. You can pass either the signature ID or its name. ## Warmup Links **GET** `/warmup-links` Headers: - `Authorization`: `YOUR_API_KEY` List warmup link configurations. FILE:references/organization.md # Organization — Users & Workspaces ## Users | Endpoint | Method | Description | |----------|--------|-------------| | `/users` | GET | List workspace users | | `/users` | POST | Invite a user | | `/users/:id` | PATCH | Update user name or role | ### Create User **POST** `/users` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "email": "[email protected]", "role": "user", "url": "https://run.salesblink.io/dashboard" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `email` | string | ✅ | Email address of the new user | | `role` | string | | `"client"`, `"user"`, `"admin"`, or `"developer"`. Default: `"user"` | | `url` | string | | Optional redirect URL after accepting invitation | > Only **owners and admins** can add users. Returns 403 otherwise. ### Update User **PATCH** `/users/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Updated Name", "role": "admin" } ``` | Field | Type | Description | |-------|------|-------------| | `name` | string | New display name | | `role` | string | One of: `client`, `user`, `admin`, `developer` | --- ## Workspaces | Endpoint | Method | Description | |----------|--------|-------------| | `/workspaces` | GET | List all accessible workspaces | | `/workspaces` | POST | Create a new workspace | | `/workspaces/:id` | PATCH | Update workspace name | > **Owner only.** Returns 403 for non-owners. ### Create Workspace **POST** `/workspaces` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Sales Team" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Workspace name (min 4 characters) | ### Update Workspace **PATCH** `/workspaces/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "New Workspace Name" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | New workspace name (min 4 characters) |
Generates complete promotional emails with optimized subject lines, preview text, body copy, CTAs, and legal compliance for various campaign types.
# Promotional Email Writer ## Purpose This skill generates complete promotional email copy for marketing campaigns — subject lines, preview text, body copy, and CTAs — across multiple campaign types: product launches, flash sales, abandoned cart recovery, newsletters, seasonal campaigns, and email drip sequences. Every output is structured for conversion and includes CAN-SPAM/GDPR compliance checks. Unlike social media skills, this is purpose-built for the email channel with its unique constraints: preview pane optimization, deliverability concerns, and legal compliance requirements. ## Triggers - "写营销邮件" - "promotional email" - "email subject line" - "abandoned cart email" - "newsletter copy" - "邮件营销" - "email drip sequence" - "邮件A/B测试" - "促销邮件" - "email campaign" ## Workflow 1. Receive campaign context from user: campaign type (launch/sale/abandoned cart/newsletter/seasonal), product details, target audience, and email goal. 2. Generate subject line(s) optimized for open rate: under 50 characters, preview-pane friendly, no deceptive language. 3. Write preview text that complements (not repeats) the subject line. 4. Structure body copy for scannability: headline → greeting → hook paragraph → product value → offer details → urgency (ethical) → CTA button → footer. 5. Craft primary CTA button copy with clear action language. 6. Include unsubscribe mechanism language and sender identity in footer. 7. Run compliance review: deceptive subject line check, missing unsubscribe check, misleading claim check. 8. Deliver complete email ready for ESP (Email Service Provider) upload. ## Prompt Templates ### 1. Full Email (`full_email`) **Purpose:** Generate a complete promotional email from campaign context. **Input:** - `campaign_type` — launch / flash_sale / seasonal / newsletter / re-engagement - `product_name` — Product or offer - `promotion_details` — Discount, bundle, or offer specifics - `target_audience` — Subscriber segment - `brand_voice` — Tone: formal / casual / playful / luxury **Output:** Complete email: Subject Line | Preview Text | Body Copy (with sections) | CTA Button | Footer (with unsubscribe). ### 2. Subject Line A/B (`subject_line_ab`) **Purpose:** Generate subject line variants for open rate testing. **Input:** - `campaign_context` — Brief campaign description - `audience_segment` — Who is receiving - `count` — How many variants (default 5) **Output:** 5 subject lines labeled by approach (curiosity, benefit, urgency, personalization, question) with character counts and predicted open-rate rationale. ### 3. Email Sequence (`email_sequence`) **Purpose:** Design a multi-email drip sequence for a customer journey stage. **Input:** - `journey_stage` — Welcome / Nurture / Abandoned cart / Post-purchase / Win-back - `product_name` — Product or brand - `sequence_length` — Number of emails (typically 3–5) **Output:** Email sequence table: Email # | Timing | Subject | Body Summary | CTA | Goal. ### 4. Abandoned Cart Email (`abandoned_cart_email`) **Purpose:** Generate a recovery email for cart abandoners. **Input:** - `product_name` — Item(s) left in cart - `cart_value` — Total cart value - `abandonment_window` — Hours since abandonment - `incentive` — Optional discount or free shipping offer **Output:** Recovery email with: gentle reminder subject, product image description placeholders, benefit recap, urgency (if incentive), CTA back to cart. ### 5. Email Compliance Review (`email_compliance_review`) **Purpose:** Review draft email for deliverability and legal risks. **Input:** - `email_draft` — Complete email: subject + body + footer - `target_region` — GDPR (EU), CAN-SPAM (US), CASL (Canada), or PIPL (China) **Output:** Compliance report: Check | Status (Pass/Flag) | Issue | Suggested Fix. ## Output Format Every full email follows this deliverable structure: ``` SUBJECT LINE: [under 50 chars] PREVIEW TEXT: [complements subject, under 100 chars] [BODY] Header/Logo space Headline Greeting Hook paragraph Product/Offer section Social proof (if applicable) CTA Button → [Button text] Urgency/Scarcity note (ethical) Closing [FOOTER] Unsubscribe link language Company info Privacy policy link ``` ## Safety Rules - **NEVER** write deceptive subject lines (e.g., "Re: Your order" when it's not a reply, fake "Urgent" flags) - **NEVER** make misleading discount claims or hidden conditions - **NEVER** omit unsubscribe mechanism language — it must be clearly present - **ALWAYS** include proper sender identity (company name, physical address for CAN-SPAM) - **ALWAYS** remind user about GDPR consent requirements for EU subscribers - **ALWAYS** flag potential spam-trigger words in subject lines (e.g., "FREE!!!", "ACT NOW!!!") ## Examples ### Example 1: Full Email for Flash Sale **Input:** Campaign="618大促", Product="XX护肤品套装", Discount="满300减50", Audience="女性25-40岁", Voice="亲切温暖" **Output:** Subject "你的618专属护肤清单来了 ✨", preview "满300减50,这套搭配我们准备了很久", body with hero image placeholder, product trio showcase, discount breakdown, countdown urgency, CTA "立即抢购", full footer. ### Example 2: Abandoned Cart **Input:** Product="一双运动鞋 ¥499", Cart value="¥499", Abandonment="24小时", Incentive="包邮" **Output:** Subject "它还在等你 👟 — 免邮提醒", gentle reminder tone, product benefit recap, free shipping highlight, CTA "回到购物车". ## Related Skills - [ad-copy-ab-tester](../ad-copy-ab-tester/) — For ad copy variants (paid channel vs. owned email) - [social-caption-kit](../social-caption-kit/) — For social media promotion of the same campaign - [landing-page-copy-pro](../landing-page-copy-pro/) — For the landing page that email CTAs link to FILE:ACCEPTANCE.md # Acceptance Criteria — Promotional Email Writer - [ ] SKILL.md is self-contained (agent can operate from it alone) - [ ] All 5 prompt templates are complete with `placeholder` inputs - [ ] Safety rules address CAN-SPAM, GDPR, deceptive subjects, and unsubscribe compliance - [ ] README.md has clear install instructions + 3 usage examples - [ ] skill.json is valid JSON with all required fields - [ ] Content is unique — email channel structure differs from social/ad skills - [ ] Email sequence, abandoned cart, and compliance review are distinct features - [ ] Slugs follow naming convention (user-facing, no prefix codes) FILE:README.md # Promotional Email Writer Complete marketing email copy — subject lines, preview text, body, and CTAs for every campaign type. ## Features - Full email generation for launches, flash sales, newsletters, seasonal campaigns - Subject line A/B variants with predicted performance rationale - Email drip sequence design for customer journey stages - Abandoned cart recovery email templates - CAN-SPAM/GDPR compliance review built-in - Conversion-optimized CTA and body structure ## Install ``` openclaw skills install harrylabsj/promo-email-writer ``` ## Usage ``` 写一封618大促的营销邮件,产品是XX品牌的护肤品套装,满300减50 为这封邮件生成5个不同的标题做A/B测试 写一封弃购挽回邮件,用户加了购物车24小时没付款 设计一个3封邮件的欢迎序列,产品是SaaS订阅服务 ``` ## Platforms Email (Platform-Agnostic — works with any ESP) ## Safety CAN-SPAM compliant. No deceptive subject lines. Clear unsubscribe language. GDPR-aware. Honest offers with no hidden conditions. ## License MIT FILE:skill.json { "name": "Promotional Email Writer", "description": "Complete promotional email copy — subject lines, preview text, body copy, and CTAs for launches, flash sales, abandoned cart, newsletters, and seasonal campaigns. Conversion-focused with CAN-SPAM compliance.", "version": "1.0.0", "type": "prompt-flow", "category": "Marketing / Email Marketing", "keywords": [ "email copy", "promo email", "marketing email", "subject line", "abandoned cart", "newsletter", "email campaign", "CAN-SPAM", "email sequence", "营销邮件" ], "platforms": ["Email (Platform-Agnostic)"], "requires": {}, "requires_api": false, "author": "harrylabsj", "license": "MIT", "safety": { "no_code_execution": true, "no_network": true, "no_credentials": true, "compliance_notes": "CAN-SPAM/GDPR compliant subject lines — no deceptive openers. Clear unsubscribe mechanism language. No misleading discount claims. Proper sender identity. GDPR-aware data handling reminders." } }
The Answer Book / 答案之书 — hold a question in your mind, flip to a random page, and receive a short philosophical answer. Supports both English and Chinese; pi...
---
name: answer-book
description: The Answer Book / 答案之书 — hold a question in your mind, flip to a random page, and receive a short philosophical answer. Supports both English and Chinese; pick the language that matches the user's question. Use when the user asks for the answer book, an oracle, guidance, wants to flip to a page, or needs a yes/no/wisdom-style answer. Trigger words:answer book, the book of answers, flip a page, oracle, ask the book, 答案之书, 翻一页, 问问书, 求一个答案, 神谕.
---
# The Answer Book / 答案之书
A digital version of "The Book of Answers" with both English and Chinese pages. Hold a yes/no (or "should I") question in your mind, then flip to a random page.
## Language selection (important)
Pick the language that matches the **user's question / the language they're chatting in**:
- User wrote in English → use `--lang en` (or omit, en is default)
- User wrote in Chinese → use `--lang zh`
- Mixed / unclear → default to the language the user used in their most recent message
Never mix languages in a single answer — the book speaks one language per flip.
## Usage
```bash
# auto English
python3 scripts/get_answer.py
# explicit language
python3 scripts/get_answer.py --lang en
python3 scripts/get_answer.py --lang zh
# specific page (1-100ish, language still required for choice)
python3 scripts/get_answer.py --lang zh 42
python3 scripts/get_answer.py --lang en 42
```
The script returns a single short answer plus the page number, in the requested language.
## How to present the result
1. State the page number.
2. Show the answer line, prominently.
3. Optionally add one short sentence inviting the user to interpret it themselves — don't over-explain.
## Extending
Edit `scripts/get_answer.py` and add new entries to `ANSWERS_EN` or `ANSWERS_ZH`. Keep each entry short, declarative, and open to interpretation.
FILE:scripts/get_answer.py
#!/usr/bin/env python3
"""
The Answer Book / 答案之书 — flip to a random page and reveal a philosophical answer.
Usage:
python3 get_answer.py [--lang en|zh] [page]
Default language: en
"""
import argparse
import json
import random
import sys
ANSWERS_EN = [
"Yes.",
"No.",
"Absolutely.",
"Without a doubt.",
"Don't count on it.",
"Trust your instincts.",
"The time is right.",
"Not yet.",
"Try again later.",
"Without question.",
"It is certain.",
"Doubtful.",
"Better not tell you now.",
"Focus on something else.",
"Follow your heart.",
"Remain patient.",
"Take action now.",
"Let it go.",
"Investigate further.",
"Listen more, speak less.",
"Embrace the change.",
"Make peace with the past.",
"It will be worth the wait.",
"Pursue it with passion.",
"Walk away from it.",
"Give it your all.",
"Wait for a sign.",
"The answer lies within.",
"Trust the process.",
"Speak from the heart.",
"Definitely not.",
"It's a clear yes.",
"Forget about it.",
"It's worth fighting for.",
"Move on, gracefully.",
"Take the leap.",
"Stay where you are.",
"Look for an alternative.",
"Don't even think about it.",
"Yes, but be cautious.",
"Unquestionably.",
"Reconsider your motives.",
"Have faith.",
"Be true to yourself.",
"Now is not the time.",
"Go ahead, be bold.",
"It's time to let go.",
"Persistence will pay off.",
"Slow down.",
"Speed it up.",
"Trust the timing.",
"Look beyond the obvious.",
"Take the road less traveled.",
"Yes, with a condition.",
"It's only just begun.",
"Beyond your wildest dreams.",
"Less is more.",
"The truth will set you free.",
"There is no escape from it.",
"Get a second opinion.",
"Make a list of pros and cons.",
"Set new goals.",
"Compromise.",
"Be brave.",
"Smile, and the world smiles with you.",
"Sleep on it.",
"Take a deep breath.",
"Save your strength.",
"Choose a different path.",
"Action speaks louder than words.",
"Don't ask for permission.",
"It's none of your business.",
"Set your priorities.",
"Stop, look, and listen.",
"Have no fear.",
"Resist the urge.",
"Be grateful.",
"Apologize.",
"Ask a friend for advice.",
"Forgive yourself.",
"Be patient.",
"Surrender, then begin again.",
"Travel more.",
"Finish what you started.",
"Start over.",
"Don't change a thing.",
"It's not your concern.",
"You already know the answer.",
"Try a different approach.",
"Believe in yourself.",
"Risk it.",
"Play it safe.",
"Keep it simple.",
"Consider the consequences.",
"Good things take time.",
"Be honest with yourself.",
"Live in the moment.",
"Make your own luck.",
"There is no try, only do.",
"Silence is golden.",
"Let go of expectations.",
"Yes, if you make it so.",
]
ANSWERS_ZH = [
"是的。",
"不是。",
"毫无疑问。",
"绝对可以。",
"千万不要。",
"顺其自然。",
"时机已到。",
"再等一等。",
"改天再问。",
"无需多虑。",
"一定如此。",
"希望渺茫。",
"现在还不能告诉你。",
"把注意力放在别处。",
"听从你的心。",
"保持耐心。",
"立刻行动。",
"放下吧。",
"再深入了解一下。",
"多听少说。",
"拥抱改变。",
"与过去和解。",
"值得等待。",
"请全心投入。",
"转身离开。",
"全力以赴。",
"等一个信号。",
"答案在你心里。",
"相信过程。",
"用心去说。",
"完全不行。",
"答案是肯定的。",
"忘了它吧。",
"值得为之奋斗。",
"优雅地走开。",
"勇敢地跳出去。",
"原地不动。",
"另寻他法。",
"想都别想。",
"可以,但要谨慎。",
"毋庸置疑。",
"重新审视你的动机。",
"请保持信念。",
"忠于自己。",
"时机未到。",
"大胆去做。",
"是时候放手了。",
"坚持终有回报。",
"慢一点。",
"再快一点。",
"相信时间。",
"看穿表象。",
"走少有人走的路。",
"可以,但有条件。",
"一切才刚刚开始。",
"超乎你的想象。",
"少即是多。",
"真相会让你自由。",
"你逃不掉的。",
"听一听别人的意见。",
"把利弊列出来。",
"重新设定目标。",
"学会妥协。",
"勇敢一点。",
"微笑面对世界。",
"睡一觉再说。",
"深呼吸。",
"保留实力。",
"换一条路走。",
"行动胜于言语。",
"不必请示别人。",
"这与你无关。",
"理清优先级。",
"停下来,看一看,听一听。",
"无需畏惧。",
"克制冲动。",
"心存感激。",
"去道个歉吧。",
"找朋友聊聊。",
"请原谅自己。",
"耐心等待。",
"先放下,再重来。",
"去远方走走。",
"把开始的事做完。",
"重头再来。",
"保持原样。",
"这事不归你管。",
"你早已知道答案。",
"换个思路。",
"相信你自己。",
"冒险一试。",
"稳妥一点。",
"保持简单。",
"想想后果。",
"好事多磨。",
"对自己诚实。",
"活在当下。",
"运气要靠自己。",
"只管去做,无所谓试。",
"沉默是金。",
"放下期待。",
"你愿意,便可以。",
]
LOCALES = {
"en": {
"answers": ANSWERS_EN,
"title": "📖 The Answer Book — Page {page}",
"footer": "Close the book gently. Trust the answer.",
"page_error": "page must be an integer between 1 and {n}",
},
"zh": {
"answers": ANSWERS_ZH,
"title": "📖 答案之书 · 第 {page} 页",
"footer": "轻轻合上书。相信这个答案。",
"page_error": "页码必须是 1 到 {n} 之间的整数",
},
}
def main() -> None:
parser = argparse.ArgumentParser(description="The Answer Book / 答案之书")
parser.add_argument("--lang", choices=["en", "zh"], default="en",
help="answer language (en or zh, default: en)")
parser.add_argument("page", nargs="?", type=int, default=None,
help="optional page number; random if omitted")
args = parser.parse_args()
locale = LOCALES[args.lang]
answers = locale["answers"]
total = len(answers)
if args.page is not None:
if not 1 <= args.page <= total:
print(json.dumps(
{"error": locale["page_error"].format(n=total)},
ensure_ascii=False,
))
sys.exit(1)
page = args.page
else:
page = random.randint(1, total)
answer = answers[page - 1]
title = locale["title"].format(page=page)
display = f"{title}\n\n ✦ {answer} ✦\n\n{locale['footer']}"
print(json.dumps(
{
"lang": args.lang,
"page": page,
"total_pages": total,
"answer": answer,
"display": display,
},
ensure_ascii=False,
indent=2,
))
if __name__ == "__main__":
main()
支持同时输入多个艺人名称,自动查找各自的演唱会/巡演信息,智能识别时间和地点相近的演出组合,规划一次出行看多场演出的最优方案,并搜索对应的往返机票和演出场馆附近酒店。适用于想在一次旅途中连看多位艺人演出的用户。
---
name: multi-concert-trip-planner
description: 支持同时输入多个艺人名称,自动查找各自的演唱会/巡演信息,智能识别时间和地点相近的演出组合,规划一次出行看多场演出的最优方案,并搜索对应的往返机票和演出场馆附近酒店。适用于想在一次旅途中连看多位艺人演出的用户。
---
# Multi-Concert Trip Planner
支持多个艺人名称输入,自动在全球巡演信息中发现时间与地点相近的演出组合,帮助用户一次出行看多场演出,并搜索对应的往返机票和场馆附近酒店,输出完整的追星出行方案。
## 核心能力
- 同时接收多个艺人名称,并行搜索各自的巡演信息
- 三层信息采集:WebSearch 摘要 → WebFetch 可靠站点 → agent-browser 浏览器渲染(处理 JS 动态页面)
- 自动发现"时间窗口"内同城市或相邻城市的演出组合
- 按组合的紧凑程度和总花费排序推荐
- 为每个推荐组合搜索往返机票,输出一站式出行方案
- 为每个推荐组合搜索演出场馆附近酒店(飞猪实时报价),自动匹配入住/退房日期
- 场次变更追踪:自动与上次搜索结果做 diff,总结新增/取消/变更的场次
## 文件结构
| 文件 | 内容 |
|------|------|
| `SKILL.md`(本文件) | 工作流程总览、参数收集、综合推荐逻辑、注意事项 |
| `concert-search.md` | 第二步:演唱会搜索策略、WebFetch 规则、提取字段 |
| `combination-matching.md` | 第三步:组合匹配算法、评分权重、都市圈参考表 |
| `flight-search.md` | 第四步:flyai 机票搜索命令、返回数据解析 |
| `hotel-search.md` | 第四步-B:flyai 酒店搜索命令、返回数据解析、筛选策略 |
| `output-template.md` | 第六步:完整输出格式模板 + 特殊场景处理 + 场次变更总结 |
| `examples.md` | 7 个交互示例(多艺人+机票+酒店、单艺人、跨城、星级预算、仅酒店、agent-browser、变更追踪) |
| `BLOCKED_SITES.md` | WebFetch 失败站点记录(持续更新) |
| `diff-tracking.md` | 场次变更追踪:快照存储格式、diff 算法、变更分类规则 |
## 工作流程
### 第一步:收集用户需求
从用户请求中提取以下参数:
- **艺人/乐队名称列表**(必填,至少 1 个)— 如用户只给了 1 个艺人,正常执行搜索(退化为单艺人模式,跳过第三步的组合匹配);如给了多个艺人,则进入多艺人组合匹配流程
- **出发城市**(仅开启机票搜索时必填 — 如用户未提供且需要搜索机票,必须主动询问)
- **时间窗口偏好**(可选,默认"未来 6 个月")— 如"今年夏天"、"下半年"、"8-10 月"
- **组合容忍天数**(可选,默认 7 天)— 两场演出之间最多间隔多少天仍视为"可组合",用户可以说"最好 1 天内"或"一周内都行"。询问时提供的选项应包含 1 天(仅限连续两天)、3 天内、7 天内、14 天内等梯度
- **是否搜索机票**(可选,默认关闭)— 机票价格来自搜索引擎摘要,仅供粗略参考,准确性有限。询问时默认关闭,用户明确要求时才开启
- **是否搜索酒店**(可选,默认关闭)— 使用飞猪搜索演出场馆附近酒店,返回实时价格和预订链接。询问时默认关闭,用户明确要求时才开启
- **酒店偏好**(可选,仅开启酒店搜索时有效)— 包括:
- 星级偏好:如"四星以上"、"经济型就好"
- 每晚预算上限:如"每晚不超过 800"
- 床型偏好:如"大床房"、"双床房"
- 酒店类型:hotel(酒店)、homestay(民宿)、inn(客栈),默认 hotel
- **预算范围**(可选,仅开启机票或酒店搜索时有效)— 如"总花费 1 万以内"、"越便宜越好"
- **地区偏好**(可选)— 如"只看亚洲"、"优先日本和韩国"、"不限地区"
若用户只提供了艺人列表,至少追问出发城市。
### 第一步-B:加载上次搜索快照
→ 详见 `diff-tracking.md`
在开始搜索前,根据本次艺人列表在 `snapshots/` 目录中查找最近一次匹配的快照。如找到,加载为 `previousSnapshot` 用于搜索完成后的 diff 对比。首次搜索该艺人组合时跳过此步。
### 第二步:并行查找各艺人演唱会信息
→ 详见 `concert-search.md`
对每个艺人使用 WebSearch 搜索巡演信息(日/英/中三语查询),通过 Task 工具并行执行。采用三层降级策略:优先从搜索摘要提取信息,对可靠站点使用 WebFetch 补充,对 JS 渲染站点使用 agent-browser 浏览器抓取。结果去重、按日期排序,过滤 14 天内场次。
### 第三步:智能组合匹配(核心逻辑)
→ 详见 `combination-matching.md`
单艺人模式跳过本步骤。多艺人模式下,用滑动窗口在时间线上扫描,按城市/都市圈分组,生成候选组合并按四维评分(艺人覆盖 40%、时间紧凑 25%、地理集中 20%、售票可行 15%)排序,取前 10 个组合。
### 第四步:搜索往返机票(可选,默认跳过)
→ 详见 `flight-search.md`
仅在用户明确要求时执行。使用 `flyai search-flight` 搜索往返机票,返回实时价格和飞猪购票链接。多组合并行搜索,每组合取最便宜的 3 个选项。
### 第四步-B:搜索演出场馆附近酒店(可选,默认跳过)
→ 详见 `hotel-search.md`
仅在用户明确要求时执行。使用 `flyai search-hotel` 以场馆名称为关键词搜索附近酒店,返回实时价格和飞猪预订链接。多城市并行搜索,每城市取前 5 家,按档次分层推荐。
### 第五步:综合整理与推荐
**如果开启了机票和/或酒店搜索:** 将组合信息、机票信息、酒店信息和城际交通(如有)合并,计算每个方案的总花费估算(总花费 = 各场门票之和 + 往返机票 + 住宿费用 + 城际交通)。
**如果未开启机票和酒店搜索(默认):** 仅基于演出信息整理推荐,不涉及机票、酒店和总花费。
**推荐逻辑:**
- 首要指标:能覆盖的艺人数量(越多越好)
- 次要指标:时间紧凑度(间隔天数越少越好)
- 参考指标:地理集中度、售票可行性
- 如有机票数据,额外参考总花费
- 标注"最佳覆盖"(看到最多艺人)和"最紧凑"(间隔最短)方案
### 第六步:呈现总结
→ 详见 `output-template.md`
按标准模板输出方案(演出安排表 + 购票链接 + 机票信息 + 酒店推荐 + 总花费估算),并处理特殊场景(艺人无演出、无可组合方案、音乐节等)。
### 第七步:保存快照 + 场次变更总结
→ 详见 `diff-tracking.md`
将本次搜索结果保存为 JSON 快照文件。如存在上次快照(第一步-B 加载),自动执行 diff 对比,在输出末尾生成「场次变更总结」,包含新增/取消/场馆变更/售票状态变更/票价变更 5 类变化。首次搜索时仅保存快照并提示用户。
## 注意事项
- 出发城市仅在用户开启机票搜索时需要,不要在未开启时追问。
- 为提升效率,多个艺人的搜索必须使用 Task 工具并行执行,而非逐一串行搜索。
- agent-browser 是最重量级的信息采集手段,仅在 WebSearch 摘要和 WebFetch 都无法获取关键信息时使用。每次使用后及时 `agent-browser close` 释放资源。详见 `concert-search.md` 第三层策略和 `BLOCKED_SITES.md` 中标记为 🟢 的站点。
- 机票价格波动较大,提醒用户价格仅供参考,建议尽早预订。
- 酒店价格同样会波动(尤其是演唱会期间热门城市),提醒用户看到合适的酒店尽早预订。
- 搜索酒店时优先使用场馆名称作为 `--key-words`,确保推荐的酒店距离场馆较近,方便观演。
- 如果场馆关键词搜索结果较少,退而使用城市核心区域(如"新宿"、"涩谷"、"梅田")作为关键词补充搜索。
- 转售平台(StubHub 等)的门票价格可能高于原价,需标注说明。
- 搜索机票时考虑演出城市对应的主要机场(如东京对应 NRT/HND,伦敦对应 LHR/LGW/STN)。
- 默认展示最多 5 个组合方案 + 未能组合的场次,除非用户要求更多。
- 尊重各网站的请求限制,合理控制搜索频率。
- 如果用户指定了预算,优先过滤掉超出预算的方案。
- 组合评分算法中的权重为默认值,如用户明确偏好(如"我更在乎省钱"),应动态调整权重。
- 每次搜索结束后必须保存快照到 `snapshots/` 目录。如存在上次快照,必须在输出末尾附上场次变更总结。详见 `diff-tracking.md`。
## 交互示例
→ 详见 `examples.md`(含 7 个完整场景:多艺人+机票+酒店、单艺人+机票、多艺人跨城、带星级预算的酒店搜索、仅搜酒店不搜机票、agent-browser 处理 JS 渲染官网、场次变更追踪 diff)
FILE:examples.md
# 交互示例
## 示例 1:多艺人 + 机票 + 酒店
**用户**:"我想看 YOASOBI 和 Ado 的演唱会,最好能一趟都看了,帮我查查"
**执行步骤**:
1. 追问:出发城市是哪里?时间范围有偏好吗?要搜索机票和酒店吗?
2. 用户回答:从上海出发,今年下半年,帮我搜机票和酒店
3. 使用 Task 工具并行搜索 YOASOBI 和 Ado 的巡演信息
4. 汇总所有场次,执行组合匹配:发现两者在 8 月都有东京场次且相隔 3 天、10 月都有大阪场次且相隔 5 天
5. 对每个组合并行搜索:上海→东京/大阪的往返机票 + 场馆附近酒店
6. 综合排序,输出包含两位艺人演出 + 机票 + 酒店的组合出行方案
7. 将无法组合的场次(如 Ado 的欧洲场次)单独列出供参考
---
## 示例 2:单艺人 + 机票(无酒店)
**用户**:"我想看 Ado 的演唱会,从深圳出发"
**执行步骤**:
1. 单艺人模式,无需组合匹配
2. 搜索 Ado 的巡演信息,找到东京、大阪、首尔、台北等场次
3. 分别搜索深圳→东京、深圳→大阪、深圳→首尔、深圳→台北的往返机票
4. 按总花费(门票 + 机票)排序,输出最优方案
---
## 示例 3:多艺人组合匹配 + 跨城交通
**用户**:"帮我看看 Coldplay、Bruno Mars、Ed Sheeran 最近在亚洲有没有时间撞上的演唱会,我从北京出发"
**执行步骤**:
1. 参数明确,直接开始搜索
2. 使用 Task 工具并行搜索三位艺人的亚洲巡演信息
3. 汇总后发现:Coldplay 11月在东京、Bruno Mars 11月在东京(相隔2天)、Ed Sheeran 11月在首尔
4. 生成组合:
- 组合A(⭐最佳):东京看 Coldplay + Bruno Mars(间隔2天,同城)
- 组合B:东京 Coldplay + 首尔 Ed Sheeran(间隔5天,需跨城)
- 组合C:三人全覆盖 — 东京2场 + 首尔1场(需额外东京→首尔交通)
5. 分别搜索机票和城际交通,输出完整方案
---
## 示例 4:单艺人 + 机票 + 酒店(带星级和预算)
**用户**:"我想看 Ado 的演唱会,从深圳出发,帮我搜一下机票和酒店,酒店要四星以上,每晚预算 1500 以内"
**执行步骤**:
1. 单艺人模式,无需组合匹配
2. 搜索 Ado 的巡演信息,找到东京、大阪、首尔、台北等场次
3. 并行搜索:
- 机票:深圳→东京、深圳→大阪、深圳→首尔、深圳→台北的往返机票
- 酒店:每个城市对应场馆附近的酒店(`--hotel-stars 4,5 --max-price 1500`)
4. 按总花费(门票 + 机票 + 住宿)排序,输出最优方案
---
## 示例 5:只搜酒店,不搜机票
**用户**:"查一下周杰伦巡演,只需要搜酒店不用搜机票,住便宜点的民宿就行"
**执行步骤**:
1. 单艺人模式,仅开启酒店搜索(不开启机票搜索)
2. 搜索周杰伦的巡演信息
3. 对每个有效场次搜索场馆附近民宿(`--hotel-types homestay --sort price_asc`)
4. 输出演出信息 + 每个城市的民宿推荐(不含机票信息)
---
## 示例 6:agent-browser 处理 JS 渲染官网
**用户**:"帮我查米津玄師和绿黄色社会下半年的演唱会,从北京出发,搜机票和酒店"
**执行步骤**:
1. 使用 Task 工具并行搜索两位艺人的巡演信息
2. WebSearch 搜索摘要中获取了米津玄師的部分日程,但缺少详细场馆和售票信息
3. 发现 ticket.kenshiyonezu.jp 是官方售票页面(BLOCKED_SITES.md 标记为 JS 渲染站点 🟢),启用 agent-browser:
```bash
agent-browser open https://ticket.kenshiyonezu.jp/pages/2026_detail
agent-browser wait 3000
agent-browser snapshot -i
# 从快照中提取完整日程(日期、场馆、票价、售票状态)
agent-browser close
```
4. 绿黄色社会的官网 ryokushaka.com/live/ 同样是 JS 渲染,agent-browser 抓取补充
5. 汇总所有场次,执行组合匹配 + 机票酒店搜索,输出完整方案
---
## 示例 7:场次变更追踪(diff)
**用户**:"再帮我查一下米津玄師和绿黄色社会下半年的演唱会"(此前已搜索过同一组艺人)
**执行步骤**:
1. 收集参数:艺人列表 = [米津玄師, 緑黄色社会],沿用上次参数
2. **加载上次快照**:在 `snapshots/` 中找到 `kenshi_yonezu_ryokushaka_20260408.json`,加载为 `previousSnapshot`
3. 使用 Task 工具并行搜索两位艺人的最新巡演信息
4. 执行组合匹配,输出最新方案
5. **保存本次快照**:写入 `kenshi_yonezu_ryokushaka_20260415.json`
6. **执行 diff**:对比上次 15 场 vs 本次 17 场
- 发现:🆕 新增 3 场(绿黄色社会追加了福冈、札幌两场 + 米津玄師新增上海场)
- 发现:❌ 取消 1 场(米津玄師 12/10 名古屋场)
- 发现:🎫 售票状态变更 2 场(米津玄師 12/3 仙台 "预售"→"在售"、12/4 仙台 "预售"→"在售")
7. 在方案输出末尾附上变更总结,特别提示"仙台场已开售,建议尽快购票"
FILE:output-template.md
# 输出格式模板
在对话中输出清晰的文字总结,使用以下格式:
```
## 多艺人追星出行方案
> 搜索艺人:{艺人A}、{艺人B}、{艺人C}
> 时间范围:{范围}
> 组合容忍天数:{N} 天
---
### 方案 1 ⭐ 最佳覆盖(覆盖 {N}/{总数} 位艺人)
📍 目的地:{城市/都市圈}
📅 行程:{起始日期} — {结束日期}(共 {N} 天)
**演出安排:**
| # | 日期 | 艺人 | 场馆 | 票价 | 状态 |
|---|------|------|------|------|------|
| 1 | {日期} | {艺人A} | {场馆} | {价格} | {状态} |
| 2 | {日期} | {艺人B} | {场馆} | {价格} | {状态} |
🔗 购票链接:
- {艺人A}:{链接}
- {艺人B}:{链接}
<!-- 以下机票部分仅在用户开启机票搜索时展示 -->
✈️ 机票(飞猪实时报价):
{航空公司} {航班号} | {出发机场}→{到达机场} | {直达/中转}
去程:{出发时间} → {到达时间}({飞行时长})
回程:{出发时间} → {到达时间}({飞行时长})
往返价格:¥{价格}
🔗 购票:{飞猪链接}
🚄 城际交通(如有):{方式} | {起点}→{终点} | ¥{价格}
<!-- 机票部分结束 -->
<!-- 以下酒店部分仅在用户开启酒店搜索时展示 -->
🏨 酒店推荐(飞猪实时报价):
| 酒店 | 档次 | 每晚价格 | 位置 | 预订 |
|------|------|----------|------|------|
| {酒店名称} | {星级/档次} | {价格} | {附近地标} | [预订链接]({飞猪链接}) |
| {酒店名称} | {星级/档次} | {价格} | {附近地标} | [预订链接]({飞猪链接}) |
| ... | | | | |
住宿费用估算:{每晚价格} × {N} 晚 = ¥{总住宿费}(以最低价酒店计算)
<!-- 酒店部分结束 -->
💰 总花费估算(如有机票+酒店数据):
门票:¥{门票总计}
机票:¥{机票价格}
住宿:¥{住宿费用}({N} 晚)
城际交通:¥{交通费用}(如有)
**合计:约 ¥{总计}**
---
### 方案 2 ⭐ 最紧凑
...
---
### 未能组合的场次
以下场次在时间或地点上无法与其他艺人组合,单独列出供参考:
| 艺人 | 日期 | 城市 | 场馆 | 备注 |
|------|------|------|------|------|
| {艺人C} | {日期} | {城市} | {场馆} | 该时段无其他艺人在附近演出 |
```
## 场次变更总结(Diff)
当存在上次搜索快照时,在主方案输出之后附上变更总结。格式如下:
```
---
## 场次变更总结
> 对比上次搜索:{上次搜索日期}({N} 天前)
> 本次搜索:{本次日期}
### 概览
| 变更类型 | 数量 |
|----------|------|
| 🆕 新增场次 | {N} |
| ❌ 取消/下架 | {N} |
| 🎫 售票状态变更 | {N} |
| 🏟️ 场馆变更 | {N} |
| 💰 票价变更 | {N} |
| ✅ 未变化 | {N} |
<!-- 仅展示有变更的分类,数量为 0 的可省略 -->
### 🆕 新增场次
| 艺人 | 日期 | 城市 | 场馆 | 票价 | 状态 |
|------|------|------|------|------|------|
| {艺人} | {日期} | {城市} | {场馆} | {票价} | {状态} |
### ❌ 取消/下架场次
| 艺人 | 日期 | 城市 | 场馆 | 说明 |
|------|------|------|------|------|
| {艺人} | {日期} | {城市} | {场馆} | 上次搜索存在,本次未找到 |
### 🎫 售票状态变更(需关注)
| 艺人 | 日期 | 城市 | 上次状态 | → | 本次状态 |
|------|------|------|----------|---|----------|
| {艺人} | {日期} | {城市} | 在售 | → | 售罄 |
<!-- 「在售→售罄」和「预售→在售」是最需要用户关注的变更,加粗或额外提醒 -->
### 🏟️ 场馆变更
| 艺人 | 日期 | 城市 | 上次场馆 | → | 本次场馆 |
|------|------|------|----------|---|----------|
| {艺人} | {日期} | {城市} | {旧场馆} | → | {新场馆} |
### 💰 票价变更
| 艺人 | 日期 | 城市 | 上次票价 | → | 本次票价 |
|------|------|------|----------|---|----------|
| {艺人} | {日期} | {城市} | {旧价格} | → | {新价格} |
```
**首次搜索时(无上次快照)的提示:**
```
---
> 📸 已保存本次搜索快照({N} 场演出),下次搜索相同艺人时将自动显示场次变更。
```
## 特殊场景处理
### 场景 1:某个艺人完全没有找到演出信息
告知用户该艺人暂无公开的巡演计划,建议关注其官方社交媒体,并继续为其他艺人生成组合方案。
### 场景 2:所有艺人都有演出,但没有找到任何可组合的方案
- 列出每位艺人各自最值得去的场次
- 说明无法组合的原因(时间差距太大 / 地区完全不同)
- 建议放宽容忍天数或地区限制,询问用户是否要调整参数重试
### 场景 3:组合中包含音乐节
如果某个艺人的演出是在音乐节上(如 Summer Sonic、Coachella),标注该场次属于音乐节,提醒用户需要购买的是音乐节通票而非单场门票,并检查同一音乐节是否还有用户列表中的其他艺人出演——如果有,这将是一个高价值组合。
FILE:diff-tracking.md
# 场次变更追踪(Diff Tracking)
每次搜索完成后,将本次搜索结果保存为快照文件。下次运行时自动加载上次快照并与本次结果做 diff,在输出末尾生成「场次变更总结」。
## 快照存储
### 文件位置
快照存储在 skill 目录下的 `snapshots/` 子目录中:
```
~/.qoderwork/skills/multi-concert-trip-planner/snapshots/
├── {快照ID}.json ← 每次搜索的结果快照
└── latest.json ← 符号链接,指向最新快照(便于快速读取上次结果)
```
### 快照 ID 命名规则
快照 ID = `{艺人列表排序后用下划线连接}_{搜索日期YYYYMMDD}`
示例:`ado_yoasobi_20260408.json`
如果同一天对相同艺人列表搜索多次,后次覆盖前次。
### 快照 JSON 结构
```json
{
"snapshotId": "kenshi_yonezu_ryokushaka_backnumber_20260408",
"createdAt": "2026-04-08T15:30:00+08:00",
"artists": ["米津玄師", "緑黄色社会", "back number"],
"timeWindow": "2026 下半年",
"shows": [
{
"id": "kenshi_yonezu_20261203_sendai",
"artist": "米津玄師",
"date": "2026-12-03",
"time": "18:00",
"venue": "セキスイハイムスーパーアリーナ",
"city": "仙台",
"country": "日本",
"price": "¥9,800",
"ticketStatus": "在售",
"ticketUrl": "https://...",
"source": "WebSearch snippet"
}
],
"totalShows": 15,
"searchDuration": "约 3 分钟"
}
```
**场次 ID 生成规则:** `{艺人名拼音/英文小写}_{日期YYYYMMDD}_{城市拼音小写}`,用于跨快照匹配同一场演出。
## 工作流程
### 搜索前:加载上次快照
1. 根据当前搜索的艺人列表,在 `snapshots/` 中查找最近一次匹配的快照
2. 匹配逻辑:艺人列表排序后完全相同(忽略大小写和空格)
3. 如果找到匹配快照,加载为 `previousSnapshot`
4. 如果没有找到(首次搜索该艺人组合),跳过 diff,搜索结束后直接保存快照
**查找命令:**
```bash
ls -t ~/.qoderwork/skills/multi-concert-trip-planner/snapshots/{艺人列表快照ID前缀}*.json | head -1
```
### 搜索后:保存快照 + 执行 diff
1. 将本次搜索的所有场次整理为快照 JSON 格式
2. 写入 `snapshots/{快照ID}.json`
3. 更新 `latest.json` 指向新快照
4. 如果存在 `previousSnapshot`,执行 diff 算法
**保存命令示例:**
```bash
# 确保 snapshots 目录存在
mkdir -p ~/.qoderwork/skills/multi-concert-trip-planner/snapshots
# 写入快照文件(通过 Write 工具)
# 更新 latest.json 符号链接
ln -sf {快照ID}.json ~/.qoderwork/skills/multi-concert-trip-planner/snapshots/latest.json
```
## Diff 算法
### 匹配规则
两条场次记录被视为"同一场演出"需满足:
- **艺人相同**(忽略大小写)
- **日期相同**(精确到天)
- **城市相同**(忽略"市"/"City"后缀,如"仙台" = "仙台市")
不依赖场次 ID 做精确匹配,因为场馆名可能在不同数据源中表述不同。
### 变更分类
对比 `previousSnapshot.shows` 和当前 `currentShows`,产出 5 类变更:
| 变更类型 | 判定逻辑 | 图标 |
|----------|----------|------|
| **新增场次** | 当前有、上次无(按艺人+日期+城市匹配不到) | 🆕 |
| **取消/下架场次** | 上次有、当前无 | ❌ |
| **场馆变更** | 同一场演出但场馆名称不同 | 🏟️ |
| **售票状态变更** | 同一场演出但售票状态变化(如"预售"→"在售"、"在售"→"售罄") | 🎫 |
| **票价变更** | 同一场演出但票价区间发生变化 | 💰 |
**优先级排序:** 取消 > 新增 > 售票状态变更 > 场馆变更 > 票价变更
### 不变场次
如果某场演出在两次快照中完全一致(日期、城市、场馆、售票状态、票价均未变),归入"不变",不在 diff 中展示。
## 输出格式
→ 详见 `output-template.md`「场次变更总结」部分
Diff 总结在主方案输出之后、末尾展示。包含:
- 上次搜索时间
- 各类变更的汇总数字
- 按变更类型分组的详细变更列表
- 需要用户关注的重点变更(如"在售→售罄"需要紧急关注)
## 注意事项
- 快照仅记录场次信息,不记录机票/酒店数据(这些实时数据每次搜索都不同,不适合做 diff)
- 如果两次搜索的时间窗口不同(如上次搜"下半年",这次搜"10-12月"),diff 时只比较两次时间窗口的交集部分,避免因搜索范围缩小而产生大量虚假"取消"
- 如果用户增减了艺人列表(如上次搜 A+B,这次搜 A+B+C),新增艺人的场次全部标为"新增",其余艺人正常做 diff
- 快照文件较小(通常 <10KB),无需定期清理。如需手动清理:`ls ~/.qoderwork/skills/multi-concert-trip-planner/snapshots/`
- 首次搜索某组艺人时,无 diff 输出,仅保存快照并提示"已保存本次搜索快照,下次搜索相同艺人时将自动显示场次变更"
FILE:combination-matching.md
# 智能组合匹配算法
**单艺人模式:** 如果用户只输入了 1 个艺人,跳过本步骤,直接进入机票/酒店搜索(如开启)或输出呈现。
**多艺人模式:** 将所有艺人的有效演出场次汇总后,执行组合匹配算法。
## 1. 定义"可组合"条件
两场(或多场)演出被视为"可组合"需满足:
1. **时间相近**:演出日期之间的间隔 ≤ 用户设定的容忍天数(默认 7 天)
2. **地点相近**:满足以下任一条件:
- **同城市**:在同一个城市(如都在东京)
- **同都市圈**:在已知的相邻城市群内(如东京-横滨、大阪-神户-京都、纽约-新泽西、伦敦-伯明翰等)
- **同国家且交通便利**:在同一国家内,高铁/飞机 3 小时内可达
3. **不同艺人**:每个组合必须包含至少 2 个不同艺人的演出
## 2. 组合生成策略
1. 将所有演出按日期排序,形成一个时间线
2. 用滑动窗口(窗口大小 = 容忍天数)在时间线上扫描
3. 对窗口内的演出,按城市/都市圈分组
4. 在每个地理分组中,检查是否包含 ≥ 2 个不同艺人
5. 如果是,生成一个组合候选
## 3. 组合评分
对每个候选组合计算综合评分,权重如下:
| 因素 | 权重 | 说明 |
|------|------|------|
| 艺人覆盖数 | 40% | 覆盖的艺人越多得分越高;覆盖全部艺人为满分 |
| 时间紧凑度 | 25% | 演出之间间隔天数越少越好 |
| 地理集中度 | 20% | 同城 > 同都市圈 > 同国家跨城 |
| 售票可行性 | 15% | 全部在售 > 部分预售 > 包含售罄场次 |
### 3.1 地区降权系数
在基础评分之上,对特定地区的场次施加降权系数(乘法修正)。降权不影响场次的搜索和展示,仅影响组合排序优先级。
| 地区 | 降权系数 | 说明 |
|------|----------|------|
| 台湾(台北、新北、桃园、高雄、台南等) | **×0.6** | 大陆用户前往台湾需办理通行证和签注,手续周期较长、存在不确定性,出行便利性低于大陆及免签/落地签目的地 |
**计算规则:**
1. 先按上方四维权重计算基础综合评分 `baseScore`
2. 检查组合中所有场次的举办地区
3. 如果组合中**任一场次**位于降权地区,最终评分 = `baseScore × 该地区降权系数`
4. 如果组合中涉及**多个不同降权地区**,取最低的降权系数
5. 不在降权表中的地区系数为 1.0(无影响)
**示例:**
- 组合 A(北京+温州):baseScore 85 × 1.0 = **85**
- 组合 B(北京+台北):baseScore 90 × 0.6 = **54** → 排在组合 A 之后
> **维护说明:** 降权系数可根据实际出行便利性调整。如未来台湾自由行政策放宽,可适当提高系数(如 0.8)。如需对其他地区降权(如需要复杂签证的国家),在表中追加即可。
按综合评分从高到低排序,取前 **10 个** 组合进入下一步。
## 4. 常见都市圈参考表
在判断"地点相近"时,参考以下都市圈映射:
**日本:**
| 都市圈 | 包含城市 |
|--------|----------|
| 东京圈 | 东京、横滨、埼玉、千叶、川崎 |
| 关西圈 | 大阪、京都、神户、奈良 |
| 名古屋圈 | 名古屋、丰田 |
| 福冈圈 | 福冈、北九州 |
**韩国:**
| 都市圈 | 包含城市 |
|--------|----------|
| 首尔圈 | 首尔、仁川、京畿道、高阳、水原 |
| 釜山圈 | 釜山、蔚山 |
**中国大陆:**
| 都市圈 | 包含城市 |
|--------|----------|
| 长三角 | 上海、杭州、南京、苏州、无锡 |
| 大湾区 | 深圳、广州、东莞、佛山 |
| 京津冀 | 北京、天津 |
| 成渝 | 成都、重庆 |
**港澳台:**
| 都市圈 | 包含城市 |
|--------|----------|
| 港澳 | 香港、澳门 |
| 台湾北部 | 台北、新北、桃园 |
| 台湾南部 | 高雄、台南 |
**北美:**
| 都市圈 | 包含城市 |
|--------|----------|
| 纽约圈 | 纽约、新泽西、布鲁克林、长岛、纽瓦克 |
| 洛杉矶圈 | 洛杉矶、安纳海姆、英格尔伍德、帕萨迪纳 |
| 旧金山湾区 | 旧金山、奥克兰、圣何塞 |
| 芝加哥圈 | 芝加哥、罗斯蒙特 |
| 多伦多圈 | 多伦多、密西沙加、汉密尔顿 |
**欧洲:**
| 都市圈 | 包含城市 |
|--------|----------|
| 伦敦圈 | 伦敦、温布利、克罗伊登 |
| 巴黎圈 | 巴黎、圣但尼、楠泰尔 |
| 莱茵-鲁尔 | 杜塞尔多夫、科隆、多特蒙德、埃森 |
| 兰斯塔德 | 阿姆斯特丹、鹿特丹、乌得勒支 |
**东南亚:**
| 都市圈 | 包含城市 |
|--------|----------|
| 新加坡 | 新加坡 |
| 曼谷圈 | 曼谷、暖武里 |
| 雅加达圈 | 雅加达、茂物 |
| 马尼拉圈 | 马尼拉、奎松、帕赛 |
遇到不在表中的城市时,用常识判断是否属于同一都市圈或交通便利区域。
FILE:hotel-search.md
# 酒店搜索(flyai)
**本步骤仅在用户明确要求搜索酒店时执行。**
对排名前列的组合方案,使用 `flyai` CLI 工具(飞猪旅行)搜索演出城市的酒店。该工具返回实时价格和预订链接。
## 日期策略
- 入住日期:第一场演出的前一天(与机票去程日期一致)
- 退房日期:最后一场演出的后一天(与机票回程日期一致)
- 如果组合中涉及多个城市,为每个城市分别搜索酒店,入住/退房日期按该城市的演出安排确定
## 搜索命令
```bash
flyai search-hotel \
--dest-name {目的地城市} \
--key-words {场馆名称或地标} \
--check-in-date {入住日期 YYYY-MM-DD} \
--check-out-date {退房日期 YYYY-MM-DD} \
--sort price_asc
```
参数说明:
- `--dest-name` 目的地城市名称,如"东京"、"大阪"、"首尔"
- `--key-words` 使用场馆名称作为关键词,确保搜索到场馆附近的酒店(如"东京巨蛋"、"大阪城ホール")
- `--sort price_asc` 按价格从低到高排序(默认推荐,也可根据用户需要改为 `distance_asc`/`rate_desc`)
- 如有星级要求,加 `--hotel-stars {1-5}`(逗号分隔,如 `--hotel-stars 4,5`)
- 如有每晚预算上限,加 `--max-price {金额}`
- 如有床型偏好,加 `--hotel-bed-types {king/twin/multi}`
- 如有酒店类型偏好,加 `--hotel-types {hotel/homestay/inn}`
**多个组合/城市的酒店搜索应并行执行。**
## 返回数据解析
flyai 返回 JSON 格式,从 `data.itemList` 数组中提取每个酒店的以下字段:
| 字段 | JSON 路径 | 说明 |
|------|-----------|------|
| 酒店名称 | `name` | 如 "东京巨蛋酒店" |
| 价格 | `price` | 每晚价格字符串(如 "¥1505") |
| 地址 | `address` | 酒店详细地址 |
| 星级/档次 | `star` | 如 "经济型"、"舒适型"、"高档型"、"豪华型" |
| 附近地标 | `interestsPoi` | 如 "近东京巨蛋"、"近秋叶原" |
| 预订链接 | `detailUrl` | 飞猪直达链接 |
| 装修时间 | `decorationTime` | 可选,如 "2023" |
| 品牌 | `brandName` | 可选,如 "万豪"、"希尔顿"(可能为 null) |
## 筛选与推荐策略
1. 每个城市取 **前 5 家** 酒店推荐(默认按价格排序)
2. 优先展示 `interestsPoi` 中包含场馆名称或附近地标的酒店
3. 按档次分层推荐:至少各展示 1 家经济型/舒适型和 1 家高档型/豪华型(如有),满足不同预算需求
4. 如用户指定了星级或预算,严格按条件过滤后再推荐
5. 计算总住宿费用时,使用每晚价格 × 入住晚数(从价格字符串中提取数字)
## 搜索补充策略
- 搜索酒店时优先使用场馆名称作为 `--key-words`,确保推荐的酒店距离场馆较近,方便观演
- 如果场馆关键词搜索结果较少,退而使用城市核心区域(如"新宿"、"涩谷"、"梅田")作为关键词补充搜索
- 酒店价格会波动(尤其是演唱会期间热门城市),提醒用户看到合适的酒店尽早预订
FILE:BLOCKED_SITES.md
# WebFetch 失败站点记录
以下网站在使用 WebFetch 抓取时无法获取有效内容。根据失败类型,采取不同降级策略:仅用 WebSearch 摘要,或使用 agent-browser 浏览器抓取。
> 最后测试时间:2026-04
## 超时(Timeout)— 仅用 WebSearch 摘要
| 站点 | URL 示例 | 失败原因 |
|------|----------|----------|
| lignea.co.jp | https://lignea.co.jp/ryokushaka/ | WebFetch 超时(>60s),2026-04 复测仍超时 |
| sonymusic.co.jp | https://www.sonymusic.co.jp/artist/ryokusyaka/info/581667 | WebFetch 超时(>60s)。注:同域名 `/live/` 路径属于 JS 渲染类型,见下方 agent-browser 一节 |
> 超时类站点不建议用 agent-browser(大概率加载也极慢),优先用 WebSearch 摘要。
## HTTP 错误 — 仅用 WebSearch 摘要
| 站点 | URL 示例 | 失败原因 |
|------|----------|----------|
| reissuerecords.net | https://reissuerecords.net/ | HTTP 403 Forbidden,服务器拒绝访问 |
> HTTP 403/5xx 类错误通常与 User-Agent 或 IP 限制有关,agent-browser 可能同样被拒绝,不建议尝试。
## JS 渲染站点 — 可用 agent-browser 🟢
以下站点 WebFetch 只能获取空壳 HTML,但 agent-browser 可以完整渲染 JS 内容。**当 WebSearch 摘要信息不足时,应使用 agent-browser 抓取。**
| 站点 | URL 示例 | WebFetch 表现 | agent-browser 抓取方式 |
|------|----------|---------------|----------------------|
| ryokushaka.com | https://www.ryokushaka.com/live/ | 仅导航栏和 banner | `open` → `wait 3000` → `snapshot -i` |
| ryokushaka.com | https://www.ryokushaka.com/news/archive/?581667 | 同上 | 同上 |
| sonymusic.co.jp | https://www.sonymusic.co.jp/artist/ryokusyaka/live/ | 仅导航链接 | `open` → `wait 3000` → `snapshot -i`(日程可能需要点击展开) |
| ticket.kenshiyonezu.jp | https://ticket.kenshiyonezu.jp/pages/2026_detail | 无演出信息 | `open` → `wait 3000` → `snapshot -i` |
**agent-browser 抓取模板:**
```bash
agent-browser open {URL}
agent-browser wait 3000
agent-browser snapshot -i
# 如需翻页或展开更多内容:
# agent-browser click @eN && agent-browser wait 1000 && agent-browser snapshot -i
agent-browser close
```
## 部分可用(需注意)
| 站点 | URL 示例 | 说明 |
|------|----------|------|
| livefans.jp | https://www.livefans.jp/groups/265804 | 此前返回 504,2026-04 复测部分页面已恢复(团体页面可用 WebFetch),但艺人详情页(/artists/)仍只返回导航链接 — 可尝试 agent-browser |
## 降级策略总览
```
信息需求
├─ WebSearch 摘要足够? → 直接提取,无需访问站点
├─ 需要补充详情?
│ ├─ 目标是可靠站点? → WebFetch(rockinon.com、natalie.mu 等)
│ ├─ 目标是 JS 渲染站点? → agent-browser(见上方 🟢 标记)
│ └─ 目标是超时/403 站点? → 放弃,仅用 WebSearch 摘要
└─ 所有手段都无数据? → 标记该艺人"暂无公开巡演信息"
```
**可靠的 WebFetch 站点:** rockinon.com、natalie.mu、fashion-press.net、tower.jp、news.yahoo.co.jp、backnumber.info
FILE:concert-search.md
# 演唱会信息搜索
对每个艺人,使用 `WebSearch` 搜索其演唱会和巡演信息。**为提升效率,多个艺人的搜索应通过 Task 工具并行执行。**
## 搜索策略(每个艺人至少 8 条查询)
### 基本查询(当年 + 明年各 4 条,中日韩英四语)
**必须同时搜索当年和明年两个年份。** 绝不能只搜当年就下结论"没有后续活动"。
```
"{艺人} ライブ ツアー {当年} 日程" ← 日文搜索,覆盖日本巡演
"{艺人} concert tour {当年} dates" ← 英文搜索,覆盖欧美巡演
"{艺人} 演唱会 巡演 {当年}" ← 中文搜索,覆盖中国大陆及华语圈
"{艺人} 콘서트 투어 {当年} 일정" ← 韩文搜索,覆盖韩国巡演
"{艺人} ライブ ツアー {明年} 日程" ← 覆盖明年日本巡演
"{艺人} concert tour {明年} dates" ← 覆盖明年欧美巡演
"{艺人} 演唱会 巡演 {明年}" ← 覆盖明年中国巡演
"{艺人} 콘서트 투어 {明年} 일정" ← 覆盖明年韩国巡演
```
仅用通用查询即可覆盖主流平台(Ticketmaster、Songkick、Bandsintown、Eventernote、大麦网、Interpark、Yes24 等结果会自然出现在搜索结果中),不再逐个平台做 `site:` 限定搜索。
### 追加查询:检查最近活动上的新发表
**重大新活动经常在刚结束的live/演唱会上官宣。** 如果搜索中发现该艺人近期(过去30天内)刚完成了一场演出或活动,必须额外搜索该活动是否公布了新情报:
```
"{艺人} {近期活动名} 新情報 発表" ← 查日文新闻
"{艺人} {近期活动名} announcement new" ← 查英文新闻
"{艺人} 追加公演 新ライブ 発表 {当年}" ← 通用追加公演查询
```
这一步不可跳过。漏掉"活动现场官宣的下一场"是最常见的搜索遗漏。
## 三层信息提取策略
信息提取遵循**逐层降级**原则,尽量用最轻量的方式获取数据:
### 第一层:WebSearch 摘要提取(默认,最快)
**优先从 `WebSearch` 返回的摘要片段(snippet)中直接提取日期、场馆、城市、票价等关键信息。** 大多数情况下摘要已包含足够的结构化数据,无需额外请求。
### 第二层:WebFetch 补充详情
仅在以下情况使用 `WebFetch` 补充详情:
- 摘要信息不完整(如缺少票价或售票状态)
- 且目标网站属于**已知可靠站点**(见下方列表)
**已知可靠的 WebFetch 站点(响应快、内容可抓取):**
- rockinon.com — 日本音乐媒体,巡演报道详细
- natalie.mu — 日本娱乐新闻,演出信息全
- fashion-press.net — 票价信息详细
- tower.jp — 巡演公告完整
- news.yahoo.co.jp — 聚合各媒体报道
- backnumber.info — 巡演日程完整
**禁止 WebFetch 的站点(参见 BLOCKED_SITES.md):**
- lignea.co.jp — 超时
- sonymusic.co.jp — 超时 / 内容为空
- reissuerecords.net — HTTP 403
- 以及其他在历史执行中记录到 BLOCKED_SITES.md 的站点
### 第三层:agent-browser 浏览器抓取(JS 渲染站点专用)
当目标站点依赖 JS 动态渲染(WebFetch 只能获取空壳 HTML),**且该站点是官方信息源或数据唯一来源**时,使用 `agent-browser` 启动真实浏览器抓取完整内容。
**适用场景(参见 BLOCKED_SITES.md「可用 agent-browser」标记):**
- 艺人/乐队官方网站的巡演页面(如 ryokushaka.com/live/、ticket.kenshiyonezu.jp)
- 唱片公司的演出日程页面(如 sonymusic.co.jp/artist/.../live/)
- JS 渲染的售票平台详情页
- WebFetch 仅返回导航栏/banner 的页面
**标准抓取流程:**
```bash
# 1. 打开目标页面并等待 JS 渲染完成
agent-browser open {URL}
agent-browser wait 3000
# 2. 获取页面快照(文字内容 + 交互元素)
agent-browser snapshot -i
# 3. 如果需要查看完整日程(可能需要点击展开/翻页)
agent-browser click @eN # 点击"更多日程"按钮
agent-browser wait 1000
agent-browser snapshot -i # 重新获取内容
# 4. 完成后关闭浏览器
agent-browser close
```
**使用原则:**
- agent-browser 是最重量级的手段,仅在第一层和第二层都无法获取关键信息时使用
- 每次抓取后及时 `agent-browser close` 释放资源
- 如果同一个 Task 中需要抓取多个 JS 站点,使用命名 session 隔离:`agent-browser --session {name} open {URL}`
- 抓取结果同样提取下方"提取字段"表中的 7 个标准字段
## 提取字段
| 字段 | 说明 |
|------|------|
| 艺人 | 表演者或乐队名称 |
| 日期与时间 | 演出日期和开始时间(含时区) |
| 场馆 | 场馆名称 |
| 城市与国家 | 演出所在城市和国家 |
| 票价 | 价格区间(注明货币) |
| 售票状态 | 在售 / 售罄 / 预售 / 候补 |
| 购票链接 | 直接链接 |
## 后处理
对每个艺人的结果去重并按日期排序,过滤掉**今天起 14 天内(含)的场次**(太临近的演出来不及准备机票和签证),仅保留半个月后及更远的场次。
FILE:flight-search.md
# 机票搜索(flyai)
**本步骤仅在用户明确要求搜索机票时执行。**
对排名前列的组合方案,使用 `flyai` CLI 工具(飞猪旅行)搜索从用户出发城市到演出城市的往返机票。该工具返回实时价格和购票链接,数据准确性远优于 WebSearch 摘要。
## 日期策略
默认策略为去程演出前一天、回程演出后一天,但可根据演出时间和航班到达时间做**当天出行优化**,节省一晚住宿费用。
### 去程日期判断
- **默认**:第一场演出的前一天
- **可优化为当天**:如果满足以下任一条件,去程改为首场演出当天:
- 演出开始时间较晚(18:00 及以后),且存在当天中午前(12:00 前)到达目的地的航班
- 演出开始时间为下午场(14:00-17:59),且存在当天上午(10:00 前)到达目的地的航班
- **优化时的搜索方式**:同时搜索前一天和当天两个去程日期,将两者的结果合并推荐,标注当天去程的航班需注意"时间较紧"
### 回程日期判断
- **默认**:最后一场演出的后一天
- **可优化为当天**:如果满足以下任一条件,回程改为末场演出当天:
- 演出结束时间较早(17:00 前结束),且存在当天晚间(20:00 后出发)的航班
- 演出为下午场且预计 16:00 前结束,存在当天 19:00 后出发的航班
- **优化时的搜索方式**:同时搜索当天和后一天两个回程日期,将两者的结果合并推荐,标注当天回程的航班需注意"散场后需尽快赶往机场"
### 注意事项
- 当天出行优化需要已知演出的具体开始时间,如果只有日期没有时间,不做优化,使用默认的前/后一天策略
- 即使做了当天优化,仍然保留前一天去程/后一天回程的搜索结果作为"稳妥方案"供用户选择
- 到达时间需考虑从机场到场馆的交通时间(一般预留 2-3 小时),回程需考虑从场馆到机场的交通时间(一般预留 1.5-2 小时)
- 如果组合中涉及同一国家的多个城市,只搜索主要入境城市的机票(如日本搜东京或大阪入境)
## 搜索命令
```bash
flyai search-flight \
--origin {出发城市} \
--destination {目的地城市} \
--dep-date {去程日期 YYYY-MM-DD} \
--back-date {回程日期 YYYY-MM-DD} \
--sort-type 3
```
参数说明:
- `--sort-type 3` 表示按价格从低到高排序,确保最便宜的结果排在前面
- 如需仅看直飞,加 `--journey-type 1`
- 如有预算限制,加 `--max-price {金额}`
**多个组合的机票搜索应并行执行(同时发出多条 flyai 命令)。**
## 返回数据解析
flyai 返回 JSON 格式,从 `data.itemList` 数组中提取每个选项的以下字段:
| 字段 | JSON 路径 | 说明 |
|------|-----------|------|
| 往返价格 | `ticketPrice` | 含税总价(CNY) |
| 航程类型 | `journeys[].journeyType` | "直达" 或 "中转" |
| 航班号 | `journeys[].segments[].marketingTransportNo` | 如 CA927 |
| 航空公司 | `journeys[].segments[].marketingTransportName` | 如 "国航" |
| 出发机场 | `journeys[].segments[].depStationName` | 如 "首都国际机场" |
| 到达机场 | `journeys[].segments[].arrStationName` | 如 "关西国际机场" |
| 出发时间 | `journeys[].segments[].depDateTime` | 如 "2026-10-22 08:40:00" |
| 到达时间 | `journeys[].segments[].arrDateTime` | 如 "2026-10-22 12:40:00" |
| 飞行时长 | `journeys[].totalDuration` | 分钟数 |
| 购票链接 | `jumpUrl` | 飞猪直达链接 |
## 筛选策略
- 每个组合取 **最便宜的 3 个** 机票选项(直飞优先展示,中转次之)
- 如果组合中涉及跨城市移动(如东京看完一场,再去大阪看另一场),额外搜索城市间交通方案(新干线、廉航等)并估算费用
- 搜索机票时考虑演出城市对应的主要机场(如东京对应 NRT/HND,伦敦对应 LHR/LGW/STN)
FILE:snapshots/latest.json
{
"snapshotId": "jay_chou_mayday_20260408",
"createdAt": "2026-04-08T16:00:00+08:00",
"artists": ["周杰伦", "五月天"],
"timeWindow": "不限时间",
"shows": [
{
"id": "jay_chou_20260515_wenzhou",
"artist": "周杰伦",
"date": "2026-05-15",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布(参考¥580-2,380)",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260516_wenzhou",
"artist": "周杰伦",
"date": "2026-05-16",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260517_wenzhou",
"artist": "周杰伦",
"date": "2026-05-17",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260626_beijing",
"artist": "周杰伦",
"date": "2026-06-26",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260627_beijing",
"artist": "周杰伦",
"date": "2026-06-27",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260628_beijing",
"artist": "周杰伦",
"date": "2026-06-28",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260717_sanya",
"artist": "周杰伦",
"date": "2026-07-17",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260718_sanya",
"artist": "周杰伦",
"date": "2026-07-18",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260719_sanya",
"artist": "周杰伦",
"date": "2026-07-19",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260801_nanjing",
"artist": "周杰伦",
"date": "2026-08-01",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260802_nanjing",
"artist": "周杰伦",
"date": "2026-08-02",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260803_nanjing",
"artist": "周杰伦",
"date": "2026-08-03",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20261017_melbourne",
"artist": "周杰伦",
"date": "2026-10-17",
"time": "19:30",
"venue": "Marvel Stadium, Docklands",
"city": "墨尔本",
"country": "澳大利亚",
"price": "AUD $208-$748",
"ticketStatus": "4/9公开发售",
"ticketUrl": "https://www.ticketmaster.com.au/jay-chou-carnival-ii-world-tour-in-melbourne-docklands-17-10-2026/event/2500647FEE7EB74F",
"source": "Ticketmaster AU"
},
{
"id": "jay_chou_20261121_sydney",
"artist": "周杰伦",
"date": "2026-11-21",
"time": "19:30",
"venue": "ENGIE Stadium, Sydney Showground",
"city": "悉尼",
"country": "澳大利亚",
"price": "未公布",
"ticketStatus": "待公布",
"ticketUrl": "https://www.sydneyshowground.com.au/whats-on/jay-chou-carnival--world-tour/",
"source": "Sydney Showground"
},
{
"id": "mayday_20260430_beijing",
"artist": "五月天",
"date": "2026-04-30",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260501_beijing",
"artist": "五月天",
"date": "2026-05-01",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260502_beijing",
"artist": "五月天",
"date": "2026-05-02",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260503_beijing",
"artist": "五月天",
"date": "2026-05-03",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260508_beijing",
"artist": "五月天",
"date": "2026-05-08",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260509_beijing",
"artist": "五月天",
"date": "2026-05-09",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260510_beijing",
"artist": "五月天",
"date": "2026-05-10",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260511_beijing",
"artist": "五月天",
"date": "2026-05-11",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260515_beijing",
"artist": "五月天",
"date": "2026-05-15",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260516_beijing",
"artist": "五月天",
"date": "2026-05-16",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260517_beijing",
"artist": "五月天",
"date": "2026-05-17",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260518_beijing",
"artist": "五月天",
"date": "2026-05-18",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260627_taipei",
"artist": "五月天",
"date": "2026-06-27",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260628_taipei",
"artist": "五月天",
"date": "2026-06-28",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260629_taipei",
"artist": "五月天",
"date": "2026-06-29",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260704_taipei",
"artist": "五月天",
"date": "2026-07-04",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260705_taipei",
"artist": "五月天",
"date": "2026-07-05",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260706_taipei",
"artist": "五月天",
"date": "2026-07-06",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260711_taipei",
"artist": "五月天",
"date": "2026-07-11",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260712_taipei",
"artist": "五月天",
"date": "2026-07-12",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
}
],
"totalShows": 34,
"searchDuration": "约 5 分钟"
}
FILE:snapshots/jay_chou_mayday_20260408.json
{
"snapshotId": "jay_chou_mayday_20260408",
"createdAt": "2026-04-08T16:00:00+08:00",
"artists": ["周杰伦", "五月天"],
"timeWindow": "不限时间",
"shows": [
{
"id": "jay_chou_20260515_wenzhou",
"artist": "周杰伦",
"date": "2026-05-15",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布(参考¥580-2,380)",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260516_wenzhou",
"artist": "周杰伦",
"date": "2026-05-16",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260517_wenzhou",
"artist": "周杰伦",
"date": "2026-05-17",
"time": "19:00",
"venue": "温州市奥体中心主体育场",
"city": "温州",
"country": "中国",
"price": "未公布",
"ticketStatus": "待开售",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260626_beijing",
"artist": "周杰伦",
"date": "2026-06-26",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260627_beijing",
"artist": "周杰伦",
"date": "2026-06-27",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260628_beijing",
"artist": "周杰伦",
"date": "2026-06-28",
"time": "未知",
"venue": "未官宣",
"city": "北京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet (网传未确认)"
},
{
"id": "jay_chou_20260717_sanya",
"artist": "周杰伦",
"date": "2026-07-17",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260718_sanya",
"artist": "周杰伦",
"date": "2026-07-18",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260719_sanya",
"artist": "周杰伦",
"date": "2026-07-19",
"time": "未知",
"venue": "未官宣",
"city": "三亚",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260801_nanjing",
"artist": "周杰伦",
"date": "2026-08-01",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260802_nanjing",
"artist": "周杰伦",
"date": "2026-08-02",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20260803_nanjing",
"artist": "周杰伦",
"date": "2026-08-03",
"time": "未知",
"venue": "未官宣",
"city": "南京",
"country": "中国",
"price": "未知",
"ticketStatus": "等待官宣",
"ticketUrl": "",
"source": "WebSearch snippet"
},
{
"id": "jay_chou_20261017_melbourne",
"artist": "周杰伦",
"date": "2026-10-17",
"time": "19:30",
"venue": "Marvel Stadium, Docklands",
"city": "墨尔本",
"country": "澳大利亚",
"price": "AUD $208-$748",
"ticketStatus": "4/9公开发售",
"ticketUrl": "https://www.ticketmaster.com.au/jay-chou-carnival-ii-world-tour-in-melbourne-docklands-17-10-2026/event/2500647FEE7EB74F",
"source": "Ticketmaster AU"
},
{
"id": "jay_chou_20261121_sydney",
"artist": "周杰伦",
"date": "2026-11-21",
"time": "19:30",
"venue": "ENGIE Stadium, Sydney Showground",
"city": "悉尼",
"country": "澳大利亚",
"price": "未公布",
"ticketStatus": "待公布",
"ticketUrl": "https://www.sydneyshowground.com.au/whats-on/jay-chou-carnival--world-tour/",
"source": "Sydney Showground"
},
{
"id": "mayday_20260430_beijing",
"artist": "五月天",
"date": "2026-04-30",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260501_beijing",
"artist": "五月天",
"date": "2026-05-01",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260502_beijing",
"artist": "五月天",
"date": "2026-05-02",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260503_beijing",
"artist": "五月天",
"date": "2026-05-03",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/10 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260508_beijing",
"artist": "五月天",
"date": "2026-05-08",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260509_beijing",
"artist": "五月天",
"date": "2026-05-09",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260510_beijing",
"artist": "五月天",
"date": "2026-05-10",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260511_beijing",
"artist": "五月天",
"date": "2026-05-11",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/14 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260515_beijing",
"artist": "五月天",
"date": "2026-05-15",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260516_beijing",
"artist": "五月天",
"date": "2026-05-16",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260517_beijing",
"artist": "五月天",
"date": "2026-05-17",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260518_beijing",
"artist": "五月天",
"date": "2026-05-18",
"time": "19:30",
"venue": "国家体育场(鸟巢)",
"city": "北京",
"country": "中国",
"price": "¥380/580/780/980/1,380",
"ticketStatus": "预售4/17 12:25",
"ticketUrl": "https://www.damai.cn",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260627_taipei",
"artist": "五月天",
"date": "2026-06-27",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260628_taipei",
"artist": "五月天",
"date": "2026-06-28",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260629_taipei",
"artist": "五月天",
"date": "2026-06-29",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260704_taipei",
"artist": "五月天",
"date": "2026-07-04",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260705_taipei",
"artist": "五月天",
"date": "2026-07-05",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260706_taipei",
"artist": "五月天",
"date": "2026-07-06",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260711_taipei",
"artist": "五月天",
"date": "2026-07-11",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
},
{
"id": "mayday_20260712_taipei",
"artist": "五月天",
"date": "2026-07-12",
"time": "19:30",
"venue": "台北大巨蛋",
"city": "台北",
"country": "中国台湾",
"price": "NT$1,525-5,525",
"ticketStatus": "5/3 11:00 开售",
"ticketUrl": "https://tixcraft.com",
"source": "WebSearch + WebFetch"
}
],
"totalShows": 34,
"searchDuration": "约 5 分钟"
}
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,
};
Alibaba Cloud ECS extension installation skill. Supports querying available extension lists, checking if a specific extension is available, and one-click ins...
---
name: alibabacloud-ecs-install-extension
description: |
Alibaba Cloud ECS extension installation skill. Supports querying available extension lists, checking if a specific extension is available,
and one-click installation of extensions (e.g., OpenClaw, BT Panel, Python environments, etc.). Extensions are officially provided by Alibaba Cloud
with verified installation packages and scripts.
Triggers: "extension", "install", "BT Panel",
"OpenClaw", "Python", "Node.js", "package", "one-click install"
---
# ECS Extension Installation Skill
You are a professional cloud operations assistant responsible for helping users query, verify, and install Alibaba Cloud ECS extensions (OOS Packages). Follow the scenario-based workflow strictly.
## Scenario Description
This skill provides ECS extension program query and installation capabilities through Alibaba Cloud OOS (Operation Orchestration Service). Users can browse available extensions, check if a specific extension is supported, and install extensions on one or more ECS instances with a single click.
**Architecture**: ECS + OOS (Operation Orchestration Service) + Cloud Assistant
**Use Cases**:
- Query available extensions (BT Panel, OpenClaw, Node.js, Python, etc.)
- Check if a specific extension is supported
- Install extensions on single or multiple ECS instances
- Deploy development environments (Python, Node.js, Java, etc.)
- Install server management panels (BT Panel, etc.)
## Prerequisites
> **Pre-check: Aliyun CLI >= 3.3.1 required**
> Run `aliyun version` to verify >= 3.3.1. If not installed or version too low,
> see `references/cli-installation-guide.md` for installation instructions.
> Then [MUST] run `aliyun configure set --auto-plugin-install true` to enable automatic plugin installation.
>
> **Plugin Installation Fallback:** If automatic plugin installation fails (e.g., network errors like `connect: bad file descriptor`), manually install the OOS plugin:
> ```bash
> # Check if oos plugin is installed
> aliyun plugin list
> # If not listed, download and install manually:
> # 1. Visit https://github.com/aliyun/alibaba-cloud-cli/releases to find the oos plugin package
> # 2. Download the .tar.gz for your platform
> # 3. Install: aliyun plugin install --file <path-to-downloaded-plugin.tar.gz>
> # 4. Verify: aliyun plugin list (should show oos plugin)
> ```
> **Pre-check: Alibaba Cloud Credentials Required**
>
> **Security Rules:**
> - **NEVER** read, echo, or print AK/SK values (e.g., `echo $ALIBABA_CLOUD_ACCESS_KEY_ID` is FORBIDDEN)
> - **NEVER** ask the user to input AK/SK directly in the conversation or command line
> - **NEVER** use `aliyun configure set` with literal credential values
> - **ONLY** use `aliyun configure list` to check credential status
>
> ```bash
> aliyun configure list
> ```
> Check the output for a valid profile (AK, STS, or OAuth identity).
>
> **If no valid profile exists, STOP here.**
> 1. Obtain credentials from [Alibaba Cloud Console](https://ram.console.aliyun.com/manage/ak)
> 2. Configure credentials **outside of this session** (via `aliyun configure` in terminal or environment variables in shell profile)
> 3. Return and re-run after `aliyun configure list` shows a valid profile
> **Endpoint Note (Plugin Mode)**: In plugin mode, the `--endpoint` flag is typically not needed. The OOS plugin resolves endpoints automatically based on `--biz-region-id`. If endpoint resolution fails, check that the `--biz-region-id` value is a valid Alibaba Cloud region ID (e.g., `cn-hangzhou`).
---
## AI-Mode & Plugin Update
> **[MUST]** Before executing any `aliyun` CLI command in this workflow, run the following initialization commands:
>
> ```bash
> aliyun configure ai-mode enable
> aliyun configure ai-mode set-user-agent --user-agent "AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension"
> aliyun plugin update
> ```
>
> After the entire workflow is complete (all scenarios finished), disable AI-Mode:
>
> ```bash
> aliyun configure ai-mode disable
> ```
## CLI Command Standards
> **[MUST]** Before executing any CLI command, read `references/related-commands.md` for command format standards.
>
> **Key Rules:**
> - **ALL `aliyun` CLI commands** must use plugin mode (lowercase-hyphenated) for both operation names and flags. This applies to **every cloud service**, not just OOS. **Only lowercase-hyphenated format is allowed** — any other format will cause `unknown flag` or `unknown command` errors.
> - OOS commands: `list-templates`, `get-template`, `start-execution`, `list-executions` with flags `--biz-region-id`, `--template-type`, `--template-name`, etc.
> - ECS commands: `describe-instances`, `describe-regions`, `run-command`, `describe-invocations`, `describe-invocation-results`, `describe-cloud-assistant-status` with flags `--region-id`, `--instance-id`, `--command-content`, etc.
>
> **[RECOMMENDED] Flag Verification:** Run `aliyun <service> <action> --help` (e.g., `aliyun ecs run-command --help`) to confirm the exact flags supported by the installed plugin version.
## Required Permissions
This skill requires the following RAM permissions:
- `bss:DescribeOrderDetail` (query order details for billing verification)
- `ecs:DescribeCloudAssistantStatus` (check Cloud Assistant status)
- `ecs:DescribeInstances` (instance information verification)
- `ecs:DescribeInvocations` (list Cloud Assistant command invocations)
- `ecs:DescribeInvocationResults` (view command execution results)
- `ecs:RunCommand` (Cloud Assistant command execution during installation)
- `oos:GetApplicationGroup` (get OOS application group information)
- `oos:GetTemplate` (get OOS template details)
- `oos:ListInstancePackageStates` (query instance extension package status)
- `oos:ListTemplates` (list available extension packages)
- `oos:StartExecution` (start OOS execution for installation)
- `oos:UpdateInstancePackageState` (update instance package state)
- `oss:GetObject` (download extension package files from OSS)
See `references/ram-policies.md` for detailed policy configuration.
> **[MUST] Permission Failure Handling:** When any command or API call fails due to permission errors at any point during execution, follow this process:
> 1. Read `references/ram-policies.md` to get the full list of permissions required by this SKILL
> 2. Use `ram-permission-diagnose` skill to guide the user through requesting the necessary permissions
> 3. Pause and wait until the user confirms that the required permissions have been granted
## Parameter Confirmation
> **IMPORTANT: Parameter Confirmation** — Before executing any installation command,
> ALL user-customizable parameters MUST be confirmed with the user. Do NOT assume or use default
> values without explicit user approval.
| Parameter Name | Required/Optional | Description | Default Value |
|----------------|-------------------|-------------|---------------|
| `RegionId` | Required | Region where the target instances are located | N/A |
| `InstanceId` | Required | One or more ECS instance IDs to install the extension on | N/A |
| `PackageName` | Required | Extension package name (e.g., `ACS-Extension-BaoTaPanelFree-One-Click-1853370294850618`) | N/A |
| `Parameters` | Optional | Installation parameters specific to the extension (version, etc.) | Determined by template |
### Input Validation Rules
> **[MUST]** Before assembling any CLI command, validate ALL user-provided input values. Reject invalid input immediately and prompt the user to correct it. **Never** pass unvalidated user input into shell command strings.
| Parameter | Validation Rule | Example |
|-----------|----------------|---------|
| `InstanceId` | Must match regex `^i-[a-zA-Z0-9]{10,30}$`. Each ID in the array must pass validation. | `i-bp12z30vh0wadpyv3jo3` |
| `RegionId` | Must be a valid Alibaba Cloud region ID. Validate by calling `aliyun ecs describe-regions` and checking against the returned region list. | `cn-hangzhou`, `us-east-1` |
| `PackageName` | Must match regex `^[a-zA-Z0-9][a-zA-Z0-9\-]*$` (only alphanumeric characters and hyphens, must start with alphanumeric). | `ACS-Extension-node-1853370294850618` |
| `ResourceIds` array | Maximum length: **50** instances per execution. | — |
> **Special Character Escaping:** After validation, all user-provided string values must be properly JSON-escaped (e.g., quotes, backslashes) before embedding into the `--Parameters` JSON string. Use `jq` or equivalent tools to construct the JSON payload programmatically rather than manual string concatenation when possible.
---
## Scenario-Based Routing
> **IMPORTANT: Before starting installation, identify the user's intent and follow the appropriate workflow.**
Based on the user's request, route to the appropriate scenario:
| User Intent | Trigger Keywords | Handling Method |
|-------------|------------------|-----------------|
| **Query Available Extensions** | "what extensions", "list", "available extensions", "show me" | Execute **Scenario 1** |
| **Query Extension Support** | "can I install", "is it supported", "do you have", "support" | Execute **Scenario 2** |
| **Install Extension** | "install", "deploy", "one-click install", "set up" | Execute **Scenario 3** |
---
## Scenario 1: Query Available Extensions List
When the user asks "What extensions are available?" or similar, follow these steps:
### Step 1: List Templates
Call `list-templates` to get all available public extension packages:
```bash
aliyun oos list-templates \
--biz-region-id cn-hangzhou \
--template-type Package \
--share-type Public \
--max-results 100 \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
```
### Step 2: Parse and Display Results
Parse the response and present the results in a table format to the user:
| Extension Name | Description | Category |
|----------------|-------------|----------|
| (from TemplateName, prefer `name-zh-cn` from parsed Description JSON) | (from `zh-cn` or `en` in parsed Description JSON) | (from `categories` in parsed Description JSON) |
> **Note:** The `Description` field is a JSON string containing metadata. Parse it to extract:
> - `name-zh-cn`: Chinese display name (preferred for display)
> - `name-en`: English display name
> - `zh-cn`: Chinese description
> - `en`: English description
> - `categories`: Category tags array
> - `doc-zh-cn`: Chinese documentation link
> - `doc-en`: English documentation link
> - `image`: Icon URL
>
> Example `Description` value:
> ```json
> "Description": "{\"categories\":[\"application\"],\"en\":\"BaoTa Panel free edition one-click installation\",\"zh-cn\":\"BaoTa Panel free edition one-click installation\",\"name-en\":\"BaoTaPanelFree-One-Click\",\"name-zh-cn\":\"BaoTaPanelFree-One-Click\",\"image\":\"https://oos-public-template.oss-cn-beijing.aliyuncs.com/BaoTaPanelFree/icon.png\"}"
> ```
> **Note:** The `--biz-region-id` in the command is used for API endpoint routing. The returned public templates are available across all regions.
---
## Scenario 2: Query if a Specific Extension is Supported
When the user asks "Can I install XXX?" or similar, follow these steps:
### Step 1: List and Search
Call `list-templates` (same as Scenario 1) and search for the extension by keyword:
```bash
aliyun oos list-templates \
--biz-region-id cn-hangzhou \
--template-type Package \
--share-type Public \
--max-results 100 \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
```
### Step 2: Match Results
- If matched: return the extension details (name, description, supported OS, etc.)
- If not matched: inform the user that the extension is not currently supported, and suggest similar alternatives or Scenario 1 to browse the full list
---
## Scenario 3: Install Extension
This is the core workflow. Follow these steps in strict order:
### Step 1: Confirm Extension Name
Confirm the exact extension name the user wants to install.
- If the user is unsure, execute **Scenario 1** or **Scenario 2** first to help them find the correct extension.
- If the user provides a vague name (e.g., "BT Panel"), search and confirm the exact `TemplateName` (e.g., `ACS-Extension-BaoTaPanelFree-One-Click-1853370294850618`).
### Step 2: Get Template Details
Call `get-template` to retrieve the extension template details. **Redirect output to a temporary file** to avoid terminal truncation (the `Content` field is usually very large):
```bash
aliyun oos get-template \
--biz-region-id cn-hangzhou \
--template-name "【Extension-Name】" \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension > /tmp/oos-template.json
```
Then extract the `Parameters` from the template content:
```bash
jq -r '(.Content | fromjson | .Parameters)' /tmp/oos-template.json
```
> **[IMPORTANT] Output Truncation Warning**: `get-template` returns a `Content` field that is typically very large (contains full installation scripts). Always redirect command output to a temporary file (`> /tmp/oos-template.json`) first, then use `jq` or file read tools to parse. Do **not** rely on terminal output directly — truncated JSON will cause parsing errors.
The `Content` field (JSON string) includes:
- `Parameters`: defines the installation parameters required (e.g., version number, installation path, etc.)
- `Description`: extension description
- `TemplateVersion`: template version
Parse `Content.Parameters` and extract all required and optional parameters.
### Step 3: Guide User to Provide Parameters
Based on the `Parameters` parsed in Step 2, guide the user to provide necessary values:
- **Required parameters**: must obtain user input
- **Optional parameters**: inform the user of defaults; if the user does not provide, use defaults
> **[IMPORTANT]** Only extract parameters from `Content.Parameters`. Do **not** infer parameters from `InstallScript` or other template content — shell variables inside scripts are internal implementation details, not user-configurable parameters.
Common parameter examples:
| Parameter | Type | Description |
|-----------|------|-------------|
| `version` | String | Software version number (e.g., `v22.13.1` for Node.js) |
| `packageVersion` | String | Extension package version (e.g., `v27`) |
> **Note:** Do not fabricate parameter values. Must be obtained from the user or template defaults.
### Step 4: Confirm All Parameters
> **[MUST]** Before executing the installation, you MUST output a parameter confirmation table to the user containing ALL of the following items and explicitly ask **"Please confirm the above parameters are correct before I proceed with installation."** You MUST NOT proceed to Step 5 until the user provides an affirmative response. Even if the user has already provided all parameters in their initial request, the confirmation step is still mandatory.
| Item | Value |
|------|-------|
| RegionId | (User provided) |
| InstanceId(s) | (User provided, supports multiple) |
| Extension Name (PackageName) | (Confirmed in Step 1) |
| Installation Parameters | (From Step 2/3, including version and any default values being used) |
> **[MUST] Instance Count Verification:** Verify that the number of InstanceIds matches the user's request. If the user mentions N instances but provides fewer IDs, ask for the missing instance IDs before proceeding.
>
> **[MUST]** Installation operations will modify instance state. Must obtain explicit user confirmation before execution. Do NOT skip this step under any circumstances.
### Step 5: Execute Installation
> **[MUST] Idempotency Check:** Before executing, query whether a running execution already exists for the same extension and target instances:
>
> ```bash
> aliyun oos list-executions \
> --biz-region-id "【User-Provided-Region】" \
> --template-name "ACS-ECS-BulkyConfigureOOSPackageWithTemporaryURL" \
> --status Running \
> --user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
> ```
>
> If a running execution with the same `packageName` and `targets` is found:
> 1. Inform the user about the existing execution
> 2. Ask the user whether to wait for it or create a new execution
> 3. **If the user does not respond or confirms to proceed, you MUST still call `start-execution` to create a new execution — do NOT skip `start-execution` under any circumstances**
>
> **The `start-execution` call is the mandatory core action of this step and must always be executed unless the user explicitly requests to wait for the existing execution.**
>
> **[RECOMMENDED] ClientToken:** Generate a deterministic `ClientToken` to prevent duplicate submissions caused by retries. The `ClientToken` must be a string of 1-64 ASCII characters.
>
> ```bash
> # Generate a deterministic ClientToken and save it for reuse
> CLIENT_TOKEN="regionId-packageName-$(date +%Y%m%d%H%M)"
>
> # All subsequent retries reuse the same token, ensuring idempotency
> aliyun oos start-execution \
> ... \
> --client-token "$CLIENT_TOKEN"
> ```
>
> This ensures that no matter how many times the command is retried, the same installation intent always maps to the same token.
**[MUST]** Call `start-execution` to execute the installation task (this call must NOT be skipped):
**[MUST] Parameter Recording:** Before executing `start-execution`, save the complete `--parameters` JSON to a file for traceability, then use the file content for the command:
```bash
# Save parameters to file for traceability
cat > /tmp/oos-start-params.json << 'PARAMS_EOF'
{"regionId":"【User-Provided-Region】","OOSAssumeRole":"","targets":{"ResourceIds":["【User-Provided-InstanceId】"],"RegionId":"【User-Provided-Region】","Type":"ResourceIds"},"rateControl":{"Mode":"Concurrency","Concurrency":1,"MaxErrors":0},"action":"install","packageName":"【User-Specified-Package】","parameters":【User-Provided-Parameters】}
PARAMS_EOF
# Execute with parameters from file
aliyun oos start-execution \
--biz-region-id "【User-Provided-Region】" \
--template-name "ACS-ECS-BulkyConfigureOOSPackageWithTemporaryURL" \
--mode "Automatic" \
--tags "{}" \
--parameters "$(cat /tmp/oos-start-params.json)" \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
```
**[MUST]** After executing, log the key parameter values that were passed:
```
Parameters passed to OOS:
- packageName: <actual value>
- packageVersion: <actual value, if applicable>
- parameters.version: <actual value, if applicable>
- targets.ResourceIds: <actual value>
```
Include the complete parameters JSON (from `/tmp/oos-start-params.json`) in the Installation Report's "Installation Parameters" field.
**Parameter Description:**
| Parameter | Description |
|-----------|-------------|
| `regionId` | Must be consistent with `--biz-region-id` |
| `targets.ResourceIds` | Array of instance IDs to install on |
| `targets.RegionId` | Must be consistent with `--biz-region-id` |
| `targets.Type` | Fixed value `ResourceIds` |
| `rateControl.Concurrency` | Number of concurrent installations, default 1 |
| `rateControl.MaxErrors` | Maximum number of errors allowed, default 0 |
| `action` | Fixed value `install` |
| `packageName` | Extension package name |
| `parameters` | Extension-specific installation parameters (JSON object) |
**Example:**
```bash
aliyun oos start-execution \
--biz-region-id cn-hangzhou \
--template-name "ACS-ECS-BulkyConfigureOOSPackageWithTemporaryURL" \
--mode "Automatic" \
--tags "{}" \
--parameters "{\"regionId\":\"cn-hangzhou\",\"OOSAssumeRole\":\"\",\"targets\":{\"ResourceIds\":[\"i-bp12z30vh0xxxxxxxxxx\"],\"RegionId\":\"cn-hangzhou\",\"Type\":\"ResourceIds\"},\"rateControl\":{\"Mode\":\"Concurrency\",\"Concurrency\":1,\"MaxErrors\":0},\"action\":\"install\",\"packageName\":\"ACS-Extension-node-1853370294850618\",\"packageVersion\":\"v27\",\"parameters\":{\"version\":\"v22.13.1\"}}" \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
```
### Step 6: Check Execution Result and Verify
After the command returns, extract `ExecutionId` from the response and poll the execution status:
```bash
aliyun oos list-executions \
--biz-region-id "【User-Provided-Region】" \
--execution-id "【ExecutionId-from-Response】" \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
```
> **Polling Strategy**: Check execution status **every 20 seconds**. If the status is still `Running`, wait 20 seconds and check again. **Maximum wait time is 20 minutes** (60 checks).
>
> **[MUST] Terminal Status Requirement:** You MUST continue polling until the execution reaches a **terminal status** (`Success`, `Failed`, or `Cancelled`). While the status is `Running`, it is **absolutely forbidden** to generate the Installation Report. You may ONLY stop polling and generate a report in these two cases:
> 1. The execution has reached a terminal status (`Success`, `Failed`, or `Cancelled`)
> 2. You have polled for the full 20 minutes (60 checks at 20-second intervals) and the status is still `Running` — in this case, output a **PENDING** report with Execution Status set to `Pending (timed out after 20 minutes)` and include in Result Details: "Installation is still in progress, exceeded the 20-minute maximum wait time. Please check status manually using: `aliyun oos list-executions --biz-region-id <region> --execution-id <exec-id> --user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension`"
>
> **Any other situation (e.g., polling fewer than 60 times while status is still `Running`) absolutely forbids generating a report. You must keep polling.**
Installation status explanation:
| Status | Description |
|--------|-------------|
| `Running` | Installation in progress — wait 20 seconds and check again. **Do NOT output the report yet.** |
| `Success` | Installation successful — proceed to generate the report |
| `Failed` | Installation failed — view `Outputs` or `Tasks` for error details, then generate the report |
| `Cancelled` | Installation cancelled — generate the report |
> **[MUST] Post-Installation Version Verification:** When the execution status is `Success`, you MUST verify the actual installed/existing software version by executing the appropriate version check command via Cloud Assistant (using `aliyun ecs run-command` or the OOS_RunCommand MCP tool). This applies regardless of whether the output indicates the software was freshly installed or already existed.
>
> **Example** (verifying Node.js version via Cloud Assistant — note: ALL flags use kebab-case):
> ```bash
> aliyun ecs run-command \
> --region-id "<region>" \
> --instance-id '["<instance-id>"]' \
> --type RunShellScript \
> --command-content "node -v" \
> --user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
> ```
>
> Standard version check commands:
> | Software | Command |
> |----------|---------|
> | Node.js | `node -v` |
> | Python | `python3 --version` |
> | Java | `java -version` |
>
> **[MUST] Version Information Reporting Rules:**
> 1. Extract the complete version number from the version check command output (e.g., `v22.13.1`, `3.10.12`, `21.0.7`)
> 2. In the Installation Report's Result Details field, include version information in this exact format:
> ```
> Requested version: <version parameter specified by user>
> Actual installed/existing version: <version extracted from check command>
> Version verification: <Matches requirement / Does not match / Unable to verify>
> ```
> 3. If the actual version does not match the requested version, add a warning in Follow-up Suggestions
> 4. **All version numbers in the report MUST come from the version check command output. Do NOT infer or guess version numbers from descriptive log text. Multiple inconsistent version numbers in a single report are forbidden.**
---
## Installation Report Output Format
> **[MUST]** Only generate this report when one of the following conditions is met:
> 1. The execution has reached a terminal status (`Success`, `Failed`, `Cancelled`)
> 2. You have polled for the full 20 minutes (60 checks) and the status is still `Running` (report as `Pending (timed out after 20 minutes)`)
>
> **It is absolutely forbidden to generate this report if polling has not reached 60 checks and the status is still `Running`.** You must keep polling.
```
================== ECS Extension Installation Report ==================
【Extension Name】 : (Extension package name)
【Installation Target】 : (List of instance IDs)
【Installation Parameters】: (JSON-formatted installation parameters)
【Execution ID】 : (OOS ExecutionId)
【Execution Status】 : (Success / Failed / Cancelled / Pending-timed out)
【Completion Time】 : (Execution end time, or "N/A — still running" if timed out)
【Result Details】 : (Execution output or error information)
【Follow-up Suggestions】 :
1. (Suggestion 1, e.g., verify service status)
2. (Suggestion 2, e.g., security group port opening)
3. (Suggestion 3, e.g., check installation logs)
=======================================================================
```
## Best Practices
1. **Confirm parameters before installation** — Extension installation will modify the instance environment; must confirm all parameters with the user before execution
2. **Check instance status** — Ensure the target instance is in the `Running` state before installation
3. **Choose the correct version** — Version parameters vary by extension; obtain the correct version number from the user
4. **Multiple instances supported** — `ResourceIds` supports arrays; can install the same extension on multiple instances at once
5. **Security awareness** — Never expose AK/SK in commands or reports
## Reference Links
| Document | Description |
|----------|-------------|
| [Related Commands](references/related-commands.md) | **CLI command standards and all commands reference** |
| [RAM Policies](references/ram-policies.md) | Required RAM permissions list |
| [CLI Installation Guide](references/cli-installation-guide.md) | Aliyun CLI installation instructions |
## Notes
1. Extension installation may take several minutes; wait patiently and regularly query execution status
2. On API failure, read error messages, check permissions, and retry
3. Sensitive information (AccessKey, passwords) must never appear in reports or commands
4. Some extensions may require specific operating system versions; confirm OS compatibility in `get-template` response
5. Extension installation failures are usually caused by: instance not running, network issues, incompatible OS versions, or insufficient disk space
FILE:references/cli-installation-guide.md
# Aliyun CLI Installation & Configuration Guide
Complete guide for installing and configuring Aliyun CLI.
> **Aliyun CLI 3.3.1+**: Supports installing and using all published Alibaba Cloud product plugins. Make sure to upgrade to 3.3.1 or later for full plugin ecosystem coverage.
## Installation
### macOS
**Using Homebrew (Recommended)**
```bash
brew install aliyun-cli
# Upgrade to latest
brew upgrade aliyun-cli
# Verify version (>= 3.3.1)
aliyun version
```
**Using Binary**
```bash
# Download
wget https://aliyuncli.alicdn.com/aliyun-cli-macosx-latest-amd64.tgz
# Extract
tar -xzf aliyun-cli-macosx-latest-amd64.tgz
# Move to PATH
sudo mv aliyun /usr/local/bin/
# Verify
aliyun version
```
### Linux
**Debian/Ubuntu**
```bash
# Download
wget https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz
# Extract and install
tar -xzf aliyun-cli-linux-latest-amd64.tgz
sudo mv aliyun /usr/local/bin/
# Verify
aliyun version
```
**CentOS/RHEL**
```bash
# Download
wget https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz
# Extract and install
tar -xzf aliyun-cli-linux-latest-amd64.tgz
sudo mv aliyun /usr/local/bin/
# Verify
aliyun version
```
**ARM64 Architecture**
```bash
# Download ARM64 version
wget https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-arm64.tgz
# Extract and install
tar -xzf aliyun-cli-linux-latest-arm64.tgz
sudo mv aliyun /usr/local/bin/
```
### Windows
**Using Binary**
1. Download from: https://aliyuncli.alicdn.com/aliyun-cli-windows-latest-amd64.zip
2. Extract the ZIP file
3. Add the directory to your PATH environment variable
4. Open new Command Prompt or PowerShell
5. Verify: `aliyun version`
**Using PowerShell**
```powershell
# Download
Invoke-WebRequest -Uri "https://aliyuncli.alicdn.com/aliyun-cli-windows-latest-amd64.zip" -OutFile "aliyun-cli.zip"
# Extract
Expand-Archive -Path aliyun-cli.zip -DestinationPath C:\aliyun-cli
# Add to PATH (requires admin privileges)
$env:Path += ";C:\aliyun-cli"
[Environment]::SetEnvironmentVariable("Path", $env:Path, [System.EnvironmentVariableTarget]::Machine)
# Verify
aliyun version
```
## Configuration
### Quick Start
```bash
aliyun configure set \
--mode AK \
--access-key-id <your-access-key-id> \
--access-key-secret <your-access-key-secret> \
--region cn-hangzhou
```
All `aliyun configure` commands support non-interactive flags, which is the recommended approach —
it works in scripts, CI/CD pipelines, and agent-driven automation without hanging on stdin prompts.
**Where to Get Access Keys**
1. Log in to Aliyun Console: https://ram.console.aliyun.com/
2. Navigate to: AccessKey Management
3. Create a new AccessKey pair
4. Save the secret immediately — it's only shown once
### Configuration Modes
Aliyun CLI supports 6 authentication modes. All examples below use non-interactive flags.
#### 1. AK Mode (Access Key)
Most common mode for personal accounts and scripts.
```bash
aliyun configure set \
--mode AK \
--access-key-id LTAI5tXXXXXXXX \
--access-key-secret 8dXXXXXXXXXXXXXXXXXXXXXXXX \
--region cn-hangzhou
```
Configuration is stored in `~/.aliyun/config.json`:
```json
{
"current": "default",
"profiles": [
{
"name": "default",
"mode": "AK",
"access_key_id": "LTAI5tXXXXXXXX",
"access_key_secret": "8dXXXXXXXXXXXXXXXXXXXXXXXX",
"region_id": "cn-hangzhou",
"output_format": "json",
"language": "en"
}
]
}
```
#### 2. StsToken Mode (Temporary Credentials)
For short-lived access (tokens expire in 1-12 hours).
```bash
aliyun configure set \
--mode StsToken \
--access-key-id LTAI5tXXXXXXXX \
--access-key-secret 8dXXXXXXXXXXXXXXXXXXXXXXXX \
--sts-token v1.0:XXXXXXXXXXXXXXXX \
--region cn-hangzhou
```
Use cases: CI/CD pipelines, temporary access for external contractors, cross-account access.
#### 3. RamRoleArn Mode (Assume RAM Role)
Assume a RAM role for elevated or cross-account access.
```bash
aliyun configure set \
--mode RamRoleArn \
--access-key-id LTAI5tXXXXXXXX \
--access-key-secret 8dXXXXXXXXXXXXXXXXXXXXXXXX \
--ram-role-arn acs:ram::123456789012:role/AdminRole \
--role-session-name my-session \
--region cn-hangzhou
```
Use cases: cross-account resource access, temporary elevated privileges, role-based access control.
#### 4. EcsRamRole Mode (ECS Instance RAM Role)
Use the RAM role attached to an ECS instance — no credentials needed.
```bash
aliyun configure set \
--mode EcsRamRole \
--ram-role-name MyEcsRole \
--region cn-hangzhou
```
Requirements: must be running on an ECS instance with a RAM role attached.
Use cases: scripts and automation running on ECS instances.
#### 5. RsaKeyPair Mode (RSA Key Pair)
Use RSA key pair for authentication (generate key pair in Aliyun Console first).
```bash
aliyun configure set \
--mode RsaKeyPair \
--private-key /path/to/private-key.pem \
--key-pair-name my-key-pair \
--region cn-hangzhou
```
#### 6. RamRoleArnWithEcs Mode (ECS + RAM Role)
Combine ECS instance role with RAM role assumption for cross-account access from ECS.
```bash
aliyun configure set \
--mode RamRoleArnWithEcs \
--ram-role-name MyEcsRole \
--ram-role-arn acs:ram::123456789012:role/TargetRole \
--role-session-name my-session \
--region cn-hangzhou
```
### Environment Variables
**Highest priority** - overrides config file
**Access Key Mode**
```bash
export ALIBABA_CLOUD_ACCESS_KEY_ID=your_access_key_id
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=your_access_key_secret
export ALIBABA_CLOUD_REGION_ID=cn-hangzhou
```
**STS Token Mode**
```bash
export ALIBABA_CLOUD_ACCESS_KEY_ID=your_access_key_id
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=your_access_key_secret
export ALIBABA_CLOUD_SECURITY_TOKEN=your_sts_token
export ALIBABA_CLOUD_REGION_ID=cn-hangzhou
```
**ECS RAM Role Mode**
```bash
export ALIBABA_CLOUD_ECS_METADATA=role_name
```
**Use Case**:
- CI/CD pipelines
- Docker containers
- Temporary credential override
### Managing Multiple Profiles
**Create Named Profiles**
```bash
aliyun configure set --profile projectA \
--mode AK \
--access-key-id LTAI5tAAAAAAAA \
--access-key-secret 8dAAAAAAAAAAAAAAAAAAAAAAAA \
--region cn-hangzhou
aliyun configure set --profile projectB \
--mode AK \
--access-key-id LTAI5tBBBBBBBB \
--access-key-secret 8dBBBBBBBBBBBBBBBBBBBBBBBB \
--region cn-shanghai
```
**Use Specific Profile**
```bash
aliyun ecs describe-instances --profile projectA
export ALIBABA_CLOUD_PROFILE=projectA
aliyun ecs describe-instances # Uses projectA
```
**List and Switch Profiles**
```bash
aliyun configure list # List all profiles
aliyun configure set --current projectA # Switch default profile
```
### Credential Priority
Credentials are loaded in this order (first found wins):
1. **Command-line flag**: `--profile <name>`
2. **Environment variable**: `ALIBABA_CLOUD_PROFILE`
3. **Environment credentials**: `ALIBABA_CLOUD_ACCESS_KEY_ID`, etc.
4. **Configuration file**: `~/.aliyun/config.json` (current profile)
5. **ECS Instance RAM Role**: If running on ECS with attached role
## Verification
### Test Authentication
```bash
# Basic test - list regions
aliyun ecs describe-regions
# Expected output: JSON array of regions
```
**If successful**, you'll see:
```json
{
"Regions": {
"Region": [
{
"RegionId": "cn-hangzhou",
"RegionEndpoint": "ecs.cn-hangzhou.aliyuncs.com",
"LocalName": "China East 1 (Hangzhou)"
},
...
]
},
"RequestId": "..."
}
```
**If failed**, you'll see error messages:
- `InvalidAccessKeyId.NotFound` - Wrong Access Key ID
- `SignatureDoesNotMatch` - Wrong Access Key Secret
- `InvalidSecurityToken.Expired` - STS token expired (for StsToken mode)
- `Forbidden.RAM` - Insufficient permissions
### Debug Configuration
```bash
# Show current configuration
aliyun configure get
# Test with debug logging
aliyun ecs describe-regions --log-level=debug
# Check credential provider
aliyun configure get mode
```
## Security Best Practices
### 1. Use RAM Users (Not Root Account)
❌ **Don't**: Use Aliyun root account credentials
✅ **Do**: Create RAM users with specific permissions
```bash
# Create RAM user in console
# Attach only necessary policies
# Use RAM user's access keys
```
### 2. Principle of Least Privilege
Grant only the minimum permissions needed:
```bash
# Example: Read-only ECS access
# Attach policy: AliyunECSReadOnlyAccess
```
### 3. Rotate Access Keys Regularly
```bash
# Create new access key in RAM Console, then update configuration
aliyun configure set --access-key-id NEW_KEY --access-key-secret NEW_SECRET
# Delete old access key from console
```
### 4. Use STS Tokens for Temporary Access
```bash
aliyun configure set --mode StsToken \
--access-key-id XXXX --access-key-secret XXXX \
--sts-token XXXX --region cn-hangzhou
```
### 5. Use ECS RAM Roles When Possible
```bash
aliyun configure set --mode EcsRamRole --ram-role-name MyRole --region cn-hangzhou
```
### 6. Never Commit Credentials
```bash
# Add to .gitignore
echo "~/.aliyun/config.json" >> .gitignore
# Use environment variables in CI/CD instead
```
### 7. Secure Config File
```bash
# Restrict permissions
chmod 600 ~/.aliyun/config.json
```
## Troubleshooting
### Issue: Command Not Found
```bash
# Check installation
which aliyun
# Check PATH
echo $PATH
# Reinstall or add to PATH
```
### Issue: Authentication Failed
```bash
# Verify configuration
aliyun configure get
# Test with debug
aliyun ecs describe-regions --log-level=debug
# Check credentials in console
# Verify access key is active
```
### Issue: Permission Denied
```bash
# Error: Forbidden.RAM
# Check RAM user permissions
# Attach necessary policies in RAM console
# Example: AliyunECSFullAccess for ECS operations
```
### Issue: STS Token Expired
```bash
# Error: InvalidSecurityToken.Expired
# Reconfigure with new token
aliyun configure set --mode StsToken \
--access-key-id XXXX --access-key-secret XXXX \
--sts-token NEW_TOKEN --region cn-hangzhou
```
### Issue: Wrong Region
```bash
# Some resources may not exist in the specified region
# Check available regions
aliyun ecs describe-regions
# Update default region
aliyun configure set region cn-shanghai
```
## Advanced Configuration
### Custom Endpoint
```bash
# Use custom or private endpoint
export ALIBABA_CLOUD_ECS_ENDPOINT=ecs-vpc.cn-hangzhou.aliyuncs.com
```
### Proxy Settings
```bash
# HTTP proxy
export HTTP_PROXY=http://proxy.example.com:8080
export HTTPS_PROXY=http://proxy.example.com:8080
# No proxy for specific domains
export NO_PROXY=localhost,127.0.0.1,.aliyuncs.com
```
### Timeout Settings
```bash
# Connection timeout (default: 10s)
export ALIBABA_CLOUD_CONNECT_TIMEOUT=30
# Read timeout (default: 10s)
export ALIBABA_CLOUD_READ_TIMEOUT=30
```
## Next Steps
After installation and configuration:
1. **Install plugins** for services you need (v3.3.1+ supports all published product plugins):
```bash
aliyun plugin install --names ecs vpc rds
# List all available plugins
aliyun plugin list-remote
```
2. **Explore commands**:
```bash
aliyun ecs --help
aliyun fc --help
```
3. **Read documentation**:
- [Command Syntax Guide](./command-syntax.md)
- [Global Flags Reference](./global-flags.md)
- [Common Scenarios](./common-scenarios.md)
## References
- Official Documentation: https://help.aliyun.com/zh/cli/
- RAM Console: https://ram.console.aliyun.com/
- Access Key Management: https://ram.console.aliyun.com/manage/ak
- Plugin Repository: https://github.com/aliyun/aliyun-cli
FILE:references/ram-policies.md
# RAM Policies for ECS Extension Installation
Required RAM permissions for the ECS Extension Installation skill.
## Permission List
| Permission | Action | Description |
|------------|--------|-------------|
| `bss:DescribeOrderDetail` | Query | Query order details for extension billing verification |
| `ecs:DescribeCloudAssistantStatus` | Query | Check Cloud Assistant status on target instances |
| `ecs:DescribeInstances` | Query | Verify instance information (status, region, etc.) |
| `ecs:DescribeInvocations` | Query | List Cloud Assistant command invocations |
| `ecs:DescribeInvocationResults` | Query | View Cloud Assistant command execution results |
| `ecs:RunCommand` | Write | Execute Cloud Assistant commands during installation |
| `oos:GetApplicationGroup` | Query | Get OOS application group information |
| `oos:GetTemplate` | Query | Get detailed information of a specific OOS template |
| `oos:ListInstancePackageStates` | Query | Query instance extension package installation status |
| `oos:ListTemplates` | Query | List available OOS templates (extension packages) |
| `oos:StartExecution` | Write | Start an OOS execution to install the extension |
| `oos:UpdateInstancePackageState` | Write | Update instance extension package state |
| `oss:GetObject` | Read | Download extension package files from OSS |
## Minimum Permission Policy
Use this policy when you only need extension installation functionality:
```json
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bss:DescribeOrderDetail",
"ecs:DescribeCloudAssistantStatus",
"ecs:DescribeInstances",
"ecs:DescribeInvocations",
"ecs:DescribeInvocationResults",
"ecs:RunCommand",
"oos:GetApplicationGroup",
"oos:GetTemplate",
"oos:ListInstancePackageStates",
"oos:ListTemplates",
"oos:StartExecution",
"oos:UpdateInstancePackageState",
"oss:GetObject"
],
"Resource": "*"
}
]
}
```
## Full Permission Policy (Recommended)
Recommended for production use with additional query and monitoring permissions:
```json
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bss:DescribeOrderDetail",
"ecs:DescribeCloudAssistantStatus",
"ecs:DescribeInstances",
"ecs:DescribeInvocations",
"ecs:DescribeInvocationResults",
"ecs:RunCommand",
"oos:GetApplicationGroup",
"oos:GetTemplate",
"oos:ListExecutions",
"oos:ListInstancePackageStates",
"oos:ListTemplates",
"oos:StartExecution",
"oos:UpdateInstancePackageState",
"oss:GetObject"
],
"Resource": "*"
}
]
}
```
> **Note:** `oos:ListExecutions` is used to query execution status and history, which is helpful for tracking installation progress. `ecs:DescribeInvocationResults` is used to view Cloud Assistant command execution results. `ecs:DescribeCloudAssistantStatus` checks if Cloud Assistant is installed and running on the instance. `oos:ListInstancePackageStates` and `oos:UpdateInstancePackageState` are used for managing extension package states on instances. `oss:GetObject` is required when the extension package needs to be downloaded from OSS. `bss:DescribeOrderDetail` is used for billing and order verification when installing paid extensions.
## Permission Verification Command
After attaching the policy, verify permissions:
```bash
# Verify OOS template query permission
aliyun oos list-templates \
--biz-region-id cn-hangzhou \
--template-type Package \
--share-type Public \
--max-results 10 \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
# Verify ECS instance query permission
aliyun ecs describe-instances \
--region-id cn-hangzhou \
--max-results 10 \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
```
If all commands return data successfully, permissions are correctly configured.
## Common Permission Errors and Troubleshooting
### Error: `Forbidden.RAM` / `NoPermission`
**Cause:** The RAM user does not have the required permissions.
**Solution:**
1. Log in to [RAM Console](https://ram.console.aliyun.com/)
2. Find the target RAM user
3. Click "Add Permissions"
4. Select "Custom Policy" and paste the minimum permission policy JSON above
5. Or select system policies: `AliyunOOSFullAccess` + `AliyunECSFullAccess` (broader permissions)
### Error: `Forbidden` on `oos:StartExecution`
**Cause:** Missing OOS execution permission.
**Solution:** Ensure the policy includes `oos:StartExecution` action.
### Error: `Forbidden` on `ecs:RunCommand`
**Cause:** Cloud Assistant command execution permission is missing.
**Solution:** Ensure the policy includes `ecs:RunCommand` action. The extension installation process requires Cloud Assistant to execute installation scripts on the instance.
### Error: `InvalidAccount.NotFound`
**Cause:** Incorrect AccessKey or the account does not exist.
**Solution:**
- Check if AccessKey ID is correct
- Verify if the AccessKey is active in the RAM console
- Reconfigure credentials outside of this session using `aliyun configure` interactively or via environment variables
### Using Predefined System Policies
If custom policies are not convenient, you can directly attach the following system policies:
| System Policy | Description |
|---------------|-------------|
| `AliyunOOSFullAccess` | Full OOS permissions (includes ListTemplates, GetTemplate, StartExecution, etc.) |
| `AliyunECSFullAccess` | Full ECS permissions (includes RunCommand, DescribeInstances, etc.) |
Attach method:
```bash
# Attach through RAM console or CLI
aliyun ram attach-policy-to-user \
--policy-type System \
--policy-name AliyunOOSFullAccess \
--user-name <your-ram-username> \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
```
> **Security Recommendation:** For production environments, use custom minimum permission policies instead of full-access system policies to follow the principle of least privilege.
FILE:references/related-commands.md
# OOS Related Commands Reference
CLI command reference for ECS Extension Installation skill.
## Command Format Standards
- For OOS commands, use plugin mode (lowercase-hyphenated) operation names: `list-templates`, `get-template`, `start-execution`, `list-executions`
- All OOS plugin flags use kebab-case: `--biz-region-id`, `--template-type`, `--share-type`, `--max-results`, `--template-name`, `--execution-id`, etc.
- Always include `--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension`
- OOS command format: `aliyun oos <action> --biz-region-id <region> [parameters]`
> **[RECOMMENDED] Flag Verification:** Run `aliyun oos <action> --help` to confirm exact flag names for the installed plugin version.
---
## list-templates
Query available OOS templates (extension packages).
### Command
```bash
aliyun oos list-templates \
--biz-region-id <region-id> \
--template-type Package \
--share-type Public \
--max-results <max-results> \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
```
### Parameters
| Parameter | Required | Type | Description |
|-----------|----------|------|-------------|
| `--biz-region-id` | Yes | String | Region ID, e.g., `cn-hangzhou` |
| `--template-type` | No | String | Template type, `Package` for extension packages |
| `--share-type` | No | String | Share type, `Public` for public templates |
| `--max-results` | No | Integer | Maximum number of results, range 1-100 |
| `--next-token` | No | String | Pagination token |
| `--user-agent` | Yes | String | Fixed value `AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension` |
### Output Example
```json
{
"Templates": [
{
"TemplateId": "t-xxxxxxxxxxxxxxxx",
"TemplateName": "ACS-Extension-BaoTaPanelFree-One-Click-1853370294850618",
"TemplateVersion": "v1",
"Description": "{\"categories\":[\"application\"],\"en\":\"BaoTa Panel free edition one-click installation\",\"zh-cn\":\"BaoTa Panel free edition one-click installation\",\"name-en\":\"BaoTaPanelFree-One-Click\",\"name-zh-cn\":\"BaoTaPanelFree-One-Click\",\"image\":\"https://oos-public-template.oss-cn-beijing.aliyuncs.com/BaoTaPanelFree/icon.png\"}",
"ShareType": "Public",
"TemplateType": "Package",
"CreatedDate": "2024-01-15T08:00:00Z",
"UpdatedDate": "2024-06-01T10:00:00Z"
},
{
"TemplateId": "t-yyyyyyyyyyyyyyyy",
"TemplateName": "ACS-Extension-node-1853370294850618",
"TemplateVersion": "v27",
"Description": "{\"categories\":[\"application\"],\"en\":\"Node.js environment one-click installation\",\"zh-cn\":\"Node.js environment one-click installation\",\"name-en\":\"Node.js\",\"name-zh-cn\":\"Node.js\",\"image\":\"https://oos-public-template.oss-cn-beijing.aliyuncs.com/Nodejs/icon.png\"}",
"ShareType": "Public",
"TemplateType": "Package",
"CreatedDate": "2024-03-10T06:00:00Z",
"UpdatedDate": "2024-07-15T12:00:00Z"
}
],
"MaxResults": 100,
"TotalCount": 2,
"RequestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```
### Output Field Description
| Field | Description |
|-------|-------------|
| `Templates` | Array of template information |
| `TemplateId` | Unique template ID |
| `TemplateName` | Template name (used as extension package name) |
| `TemplateVersion` | Template version |
| `Description` | Template description (JSON string, see parsing notes below) |
| `ShareType` | Share type: `Public` or `Private` |
| `TemplateType` | Template type: `Package` or `Automation` |
| `TotalCount` | Total number of templates |
| `RequestId` | Request ID (for troubleshooting) |
> **Description Field Parsing:** The `Description` field is a JSON string containing localized metadata. Parse it to extract:
> - `name-zh-cn`: Chinese display name (preferred for display)
> - `name-en`: English display name
> - `zh-cn`: Chinese description
> - `en`: English description
> - `categories`: Category tags array
> - `doc-zh-cn`: Chinese documentation link
> - `doc-en`: English documentation link
> - `image`: Icon URL
### Common Errors
| Error | Cause | Solution |
|-------|-------|----------|
| `unknown endpoint for oos/<region>` | Automatic endpoint resolution failed (network issue or location service unreachable) | Verify `--biz-region-id` value is correct; if still fails, check network connectivity |
| `unknown flag: --RegionId` | Using PascalCase flag instead of kebab-case | Use `--biz-region-id` instead of `--RegionId` |
| `Forbidden.RAM` | Insufficient permissions | Ensure required RAM permissions are granted (see SKILL.md Required Permissions section) |
---
## get-template
Get detailed information of a specific OOS template.
### Command
**Recommended: redirect output to a temporary file** (the `Content` field is usually very large and will be truncated in terminal):
```bash
aliyun oos get-template \
--biz-region-id <region-id> \
--template-name <template-name> \
[--template-version <version>] \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension > /tmp/oos-template.json
```
Then extract parameters using `jq`:
```bash
# Extract installation parameters
jq -r '(.Content | fromjson | .Parameters)' /tmp/oos-template.json
# Extract template description
jq -r '.Description' /tmp/oos-template.json
```
> **[IMPORTANT] Output Truncation Warning**: `get-template` returns a `Content` field that contains full installation scripts and can be extremely large. Always redirect to a file first, then parse with `jq` or file read tools. Do **not** rely on terminal output directly.
### Parameters
| Parameter | Required | Type | Description |
|-----------|----------|------|-------------|
| `--biz-region-id` | Yes | String | Region ID, e.g., `cn-hangzhou` |
| `--template-name` | Yes | String | Template name |
| `--template-version` | No | String | Template version, defaults to latest if not specified |
| `--user-agent` | Yes | String | Fixed value `AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension` |
### Output Example
```json
{
"Template": {
"TemplateId": "t-xxxxxxxxxxxxxxxx",
"TemplateName": "ACS-Extension-node-1853370294850618",
"TemplateVersion": "v27",
"Description": "{\"categories\":[\"application\"],\"en\":\"Node.js environment one-click installation\",\"zh-cn\":\"Node.js environment one-click installation\",\"name-en\":\"Node.js\",\"name-zh-cn\":\"Node.js\",\"image\":\"https://oos-public-template.oss-cn-beijing.aliyuncs.com/Nodejs/icon.png\"}",
"Content": "{\"FormatVersion\":\"OOS-2019-06-01\",\"Description\":\"Node.js environment installation\",\"Parameters\":{\"version\":{\"Type\":\"String\",\"Description\":\"Node.js version number\",\"Default\":\"v22.13.1\"}},\"Tasks\":[...]}",
"CreatedDate": "2024-03-10T06:00:00Z",
"UpdatedDate": "2024-07-15T12:00:00Z"
},
"RequestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```
### Content Field Parsing
The `Content` field is a JSON string containing the complete template definition. Key fields:
```json
{
"FormatVersion": "OOS-2019-06-01",
"Description": "Template description",
"Parameters": {
"version": {
"Type": "String",
"Description": "Parameter description",
"Default": "default value",
"AllowedValues": ["v1", "v2"]
}
},
"Tasks": [...]
}
```
| Field | Description |
|-------|-------------|
| `Parameters` | Template parameters, defines installation options |
| `Parameters.{name}.Type` | Parameter type: `String`, `Integer`, `Boolean`, etc. |
| `Parameters.{name}.Description` | Parameter description |
| `Parameters.{name}.Default` | Default value |
| `Parameters.{name}.AllowedValues` | List of allowed values |
| `Tasks` | Execution task definitions |
### Common Errors
| Error | Cause | Solution |
|-------|-------|----------|
| `TemplateNotFound` | Template name does not exist | Check if the template name is correct, use `list-templates` to query |
| `MissingTemplateName` | Missing `--template-name` parameter | Add `--template-name` parameter |
---
## start-execution
Start an OOS execution to install the extension.
### Command
```bash
aliyun oos start-execution \
--biz-region-id <region-id> \
--template-name "ACS-ECS-BulkyConfigureOOSPackageWithTemporaryURL" \
--mode "Automatic" \
--tags "{}" \
--parameters '<json-parameters>' \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
```
### Parameters
| Parameter | Required | Type | Description |
|-----------|----------|------|-------------|
| `--biz-region-id` | Yes | String | Region ID, must match the target instance region |
| `--template-name` | Yes | String | Fixed value `ACS-ECS-BulkyConfigureOOSPackageWithTemporaryURL` |
| `--mode` | Yes | String | Execution mode, `Automatic` for automatic execution |
| `--tags` | No | String | Tags, JSON format string, e.g., `"{}"` |
| `--parameters` | Yes | String | Execution parameters, JSON format string |
| `--user-agent` | Yes | String | Fixed value `AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension` |
### Parameters Field Structure
```json
{
"regionId": "cn-hangzhou",
"OOSAssumeRole": "",
"targets": {
"ResourceIds": ["i-bp12z30vh0wadpyv3jo3"],
"RegionId": "cn-hangzhou",
"Type": "ResourceIds"
},
"rateControl": {
"Mode": "Concurrency",
"Concurrency": 1,
"MaxErrors": 0
},
"action": "install",
"packageName": "ACS-Extension-node-1853370294850618",
"packageVersion": "v27",
"parameters": {
"version": "v22.13.1"
}
}
```
| Parameter | Required | Description |
|-----------|----------|-------------|
| `regionId` | Yes | Region ID, must be consistent with `--biz-region-id` |
| `OOSAssumeRole` | No | RAM role assumed by OOS, leave empty to use default |
| `targets.ResourceIds` | Yes | Array of target instance IDs |
| `targets.RegionId` | Yes | Region ID of target instances |
| `targets.Type` | Yes | Fixed value `ResourceIds` |
| `rateControl.Mode` | Yes | Rate control mode, `Concurrency` or `Batch` |
| `rateControl.Concurrency` | Yes | Number of concurrent executions |
| `rateControl.MaxErrors` | Yes | Maximum number of errors allowed |
| `action` | Yes | Fixed value `install` |
| `packageName` | Yes | Extension package name |
| `packageVersion` | No | Extension package version |
| `parameters` | No | Extension-specific parameters (JSON object) |
### Output Example
```json
{
"Execution": {
"ExecutionId": "exec-xxxxxxxxxxxxxxxx",
"TemplateName": "ACS-ECS-BulkyConfigureOOSPackageWithTemporaryURL",
"Status": "Running",
"CreateDate": "2024-08-01T10:00:00Z",
"UpdateDate": "2024-08-01T10:00:00Z",
"Parameters": {...}
},
"RequestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```
### Output Field Description
| Field | Description |
|-------|-------------|
| `ExecutionId` | Unique execution ID, used to query execution status |
| `TemplateName` | Template name |
| `Status` | Execution status: `Running`, `Success`, `Failed`, `Cancelled` |
| `CreateDate` | Execution creation time |
| `UpdateDate` | Execution update time |
| `Parameters` | Execution parameters |
### Common Errors
| Error | Cause | Solution |
|-------|-------|----------|
| `InvalidParameter` | Parameter format error | Check if `--parameters` is a valid JSON string |
| `TemplateNotFound` | Template does not exist | Check if `packageName` is correct |
| `EntityNotExists.Instance` | Instance does not exist | Check if `InstanceId` is correct |
| `InvalidInstance.NotRunning` | Instance is not in running state | Start the instance first |
| `Forbidden.RAM` | Insufficient permissions | Ensure required RAM permissions are granted (see SKILL.md Required Permissions section) |
| `RateLimit` | API rate limit exceeded | Wait a moment and retry |
---
## list-executions (Auxiliary Command)
Query OOS execution status and results.
### Command
```bash
aliyun oos list-executions \
--biz-region-id <region-id> \
--execution-id <execution-id> \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
```
### Parameters
| Parameter | Required | Type | Description |
|-----------|----------|------|-------------|
| `--biz-region-id` | Yes | String | Region ID |
| `--execution-id` | Yes | String | Execution ID returned by `start-execution` |
| `--user-agent` | Yes | String | Fixed value `AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension` |
### Output Example
```json
{
"Executions": [
{
"ExecutionId": "exec-xxxxxxxxxxxxxxxx",
"TemplateName": "ACS-ECS-BulkyConfigureOOSPackageWithTemporaryURL",
"Status": "Success",
"StatusReason": "Execution completed successfully",
"CreateDate": "2024-08-01T10:00:00Z",
"UpdateDate": "2024-08-01T10:05:00Z",
"Outputs": {
"result": "Installation completed"
},
"Tasks": [
{
"TaskName": "installPackage",
"Status": "Success",
"StatusReason": "Task completed"
}
]
}
],
"TotalCount": 1,
"RequestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```
### Execution Status Description
| Status | Description |
|--------|-------------|
| `Running` | Execution in progress |
| `Success` | Execution successful |
| `Failed` | Execution failed |
| `Cancelled` | Execution cancelled |
| `Pending` | Waiting to execute |
### Common Errors
| Error | Cause | Solution |
|-------|-------|----------|
| `ExecutionNotFound` | Execution ID does not exist | Check if the execution ID is correct |
---
## JSON Parameter Escaping Notes
When passing JSON parameters via the command line, pay attention to escaping:
### Bash
```bash
# Use single quotes to wrap the entire JSON to avoid shell escaping issues
aliyun oos start-execution \
--biz-region-id cn-hangzhou \
--template-name "ACS-ECS-BulkyConfigureOOSPackageWithTemporaryURL" \
--parameters '{"regionId":"cn-hangzhou","targets":{"ResourceIds":["i-xxx"],"RegionId":"cn-hangzhou","Type":"ResourceIds"},"action":"install","packageName":"ACS-Extension-node-1853370294850618","parameters":{"version":"v22.13.1"}}'
```
### Complex Parameters
For complex parameters, it is recommended to write them to a file first:
```bash
# Write parameters to file
cat > /tmp/oos-params.json << 'EOF'
{
"regionId": "cn-hangzhou",
"OOSAssumeRole": "",
"targets": {
"ResourceIds": ["i-bp12z30vh0wadpyv3jo3"],
"RegionId": "cn-hangzhou",
"Type": "ResourceIds"
},
"rateControl": {
"Mode": "Concurrency",
"Concurrency": 1,
"MaxErrors": 0
},
"action": "install",
"packageName": "ACS-Extension-node-1853370294850618",
"packageVersion": "v27",
"parameters": {
"version": "v22.13.1"
}
}
EOF
# Read from file
aliyun oos start-execution \
--biz-region-id cn-hangzhou \
--template-name "ACS-ECS-BulkyConfigureOOSPackageWithTemporaryURL" \
--parameters "$(cat /tmp/oos-params.json)" \
--user-agent AlibabaCloud-Agent-Skills/alibabacloud-ecs-install-extension
```
## Error Handling Best Practices
1. **API Failure Retry**: On `RateLimit` or network errors, wait 5-10 seconds and retry
2. **Permission Error**: Ensure required RAM permissions are granted, then use `ram-permission-diagnose` skill
3. **Parameter Error**: Carefully check JSON format and required fields
4. **Instance Error**: Confirm instance status is `Running` and the instance is in the correct region
5. **Execution Failure**: Use `list-executions` to query detailed error information; check `StatusReason` and `Tasks` fields
Generates structured, timed live-stream sales scripts with product intros, audience engagement, urgency cues, Q&A prep, and full session flow for live commer...
# Live Commerce Sales Script Kit ## Purpose This skill generates professional live-streaming sales scripts for live commerce hosts on platforms like Douyin Live (抖音直播), Kuaishou Live (快手直播), and Taobao Live. It covers every aspect: product introduction flow, pricing reveal cadence, urgency-building phrases (ethically constrained), audience interaction triggers, Q&A preparation, segment timing, and full-session outlines. Think of it as a director's script for your live commerce show — "Kit" signals a ready-to-use bundle of templates and frameworks, not a single monolithic output. ## Triggers - "直播带货话术" - "直播脚本" - "直播话术" - "带货脚本" - "live selling script" - "flash sale script" - "直播互动" - "单品直播" - "整场直播规划" - "逼单话术" ## Workflow 1. Receive product information and session type from user: single product demo, multi-product session, or flash sale. 2. For single product: structure the 3–8 minute product introduction flow (hook → demonstration → benefits → pricing → urgency → CTA). 3. For multi-product: build a time-allocated session outline with product sequence, transition monologues, and energy management. 4. Insert audience interaction triggers at regular intervals: polls, Q&A prompts, comment callouts, engagement games. 5. Add urgency-building phrases and transitional language — always with ethical constraints on scarcity and pricing claims. 6. Prepare anticipated audience Q&A pairs for each product. 7. Include pacing notes, segment timing, and host energy level guidance. 8. Deliver script with anchor monologue, interaction triggers, Q&A branches, and timing guide. ## Prompt Templates ### 1. Single Product Live Script (`single_product_live_script`) **Purpose:** Generate a complete 3–8 minute script for showcasing one product. **Input:** - `product_name` — Product name - `price` — Selling price (and optional original price) - `key_features` — 3–5 key selling points - `target_audience` — Who's watching - `duration_minutes` — Target segment length (3–8) **Output:** Timed script with sections: Opening Grab → Product Reveal → Feature Demo → Comparison → Pricing Reveal → Urgency Build → CTA → Transition. ### 2. Full Session Flow (`full_session_flow`) **Purpose:** Design a complete multi-product 1–4 hour live session. **Input:** - `product_list` — List of products with selling order priority - `session_duration` — Total session length in hours - `flow_style` — Energy curve: high-low-high / sustained / gradual build **Output:** Time-allocated outline with: Warm-up, Product 1-n, Intermission moments, Flash sales, Closing. Each with estimated duration, transition monologue, and energy level. ### 3. Urgency Phrase Bank (`urgency_phrase_bank`) **Purpose:** Generate a categorized bank of urgency phrases for live selling. **Input:** - `scenario` — Situation: limited-time offer / low stock / exclusive deal / first-time buyer bonus - `count` — Number of phrase variants per category **Output:** Phrases organized by category (timing-based / quantity-based / exclusivity-based), each with an ethical constraint note. ### 4. Audience Q&A Prep (`audience_qa_prep`) **Purpose:** Anticipate and prepare responses for common audience questions. **Input:** - `product_name` — Product - `product_details` — Specs, materials, sizes, guarantees - `common_concerns` — Typical buyer hesitations for this product type **Output:** 15–20 Q&A pairs organized by question type: product/details, pricing/value, logistics/after-sales, objections/skepticism. ### 5. Flash Sale Countdown (`flash_sale_countdown`) **Purpose:** Generate a high-energy countdown script for a limited-time offer. **Input:** - `product_name` — Product - `flash_price` — Flash sale price - `original_price` — Regular price - `quantity_available` — Actual available quantity - `duration_seconds` — Countdown window (typically 60–180s) **Output:** Countdown script with: Price Reveal → Quantity Mention → 30s Reminder → 10s Final Call → Sold Out / Next Product. ## Output Format All scripts follow a formatted broadcast table: | Time | Segment | Anchor Monologue | Interaction Trigger | Energy Level | |------|---------|-----------------|---------------------|--------------| | 0:00–1:00 | Opening | "Welcome..." | Ask where watching from | 🔥 High | | ... | ... | ... | ... | ... | ## Safety Rules - **NEVER** fabricate false scarcity (e.g., "only 3 left" when stock is ample) - **NEVER** invent fake original prices or price anchors to make discounts look bigger - **NEVER** use high-pressure tactics targeting vulnerable consumers (elderly, financially distressed) - **ALWAYS** prompt host to verify and disclose actual stock levels - **ALWAYS** comply with platform-specific live commerce regulations - **ALWAYS** maintain honest product descriptions — no exaggerated efficacy claims ## Examples ### Example 1: Single Product Script **Input:** Product = "XX面霜", Price = "299元 (原价399)", Features = "保湿、修护、敏感肌可用", Duration = "5分钟" **Output:** 5-minute script with opening hook about winter skin, ingredient demo, texture test, pricing reveal with savings calculation, limited-time urgency, and link click CTA. ### Example 2: Flash Sale Countdown **Input:** Product = "蓝牙耳机", Flash = "99元 (原价199)", Qty = "50件", Duration = "120s" **Output:** Countdown script with Qty count decrements at 50, 30, 10 remaining, 30s and 10s reminders, final call, and transition. ## Related Skills - [douyin-script-studio](../douyin-script-studio/) — For pre-recorded Douyin video scripts (recorded, not live) - [product-title-booster](../product-title-booster/) — For optimizing product listing titles used during live segments - [review-reply-coach](../review-reply-coach/) — For handling post-live customer feedback and reviews FILE:ACCEPTANCE.md # Acceptance Criteria — Live Commerce Sales Script Kit - [ ] SKILL.md is self-contained (agent can operate from it alone) - [ ] All 5 prompt templates are complete with `placeholder` inputs - [ ] Safety rules are explicit and actionable (NEVER/ALWAYS format) — especially false scarcity and fake price anchors - [ ] README.md has clear install instructions + 3 usage examples - [ ] skill.json is valid JSON with all required fields - [ ] Content is unique — real-time broadcast format differs from douyin-script-studio (recorded) - [ ] Flash sale, Q&A prep, and urgency phrase bank are structurally distinct features - [ ] Slugs follow naming convention (user-facing, no prefix codes) FILE:README.md # Live Commerce Sales Script Kit Professional live-streaming sales scripts for hosts — product flows, urgency phrases, Q&A prep, and full session outlines. ## Features - Single product demo scripts (3–8 minutes) with full structure - Multi-product session outlines for 1–4 hour broadcasts - Urgency-building phrase bank with ethical guardrails - Audience Q&A preparation with 15–20 anticipated questions - Flash sale countdown scripts with pacing guidance - Interaction triggers and energy management notes ## Install ``` openclaw skills install harrylabsj/live-selling-script-kit ``` ## Usage ``` 帮我写一个5分钟的单品直播脚本,产品是299元的面霜,主打保湿修护 规划一场2小时的女装直播,有8个款,给我安排流程和时间 帮我准备观众可能问的20个问题和标准回复 写一段限时秒杀的倒数话术,蓝牙耳机秒杀,50件库存 ``` ## Platforms 抖音直播, 快手直播, Taobao Live, General Live Streaming ## Safety No fake scarcity. No fabricated original prices. Honest stock disclosures. Ethical urgency language only. All scripts prompt the host to verify claims and stock before broadcast. ## License MIT FILE:skill.json { "name": "Live Commerce Sales Script Kit", "description": "Ready-to-use live streaming sales scripts — product introduction flows, pricing reveal cadence, urgency-building phrases, audience Q&A prep, and full-session outlines for live commerce hosts.", "version": "1.0.0", "type": "prompt-flow", "category": "E-Commerce / Live Commerce", "keywords": [ "live commerce", "直播带货", "直播话术", "sales script", "live selling", "抖音直播", "快手直播", "淘宝直播", "flash sale", "audience engagement", "anchor script" ], "platforms": ["抖音直播", "快手直播", "Taobao Live", "General Live Streaming"], "requires": {}, "requires_api": false, "author": "harrylabsj", "license": "MIT", "safety": { "no_code_execution": true, "no_network": true, "no_credentials": true, "compliance_notes": "No false scarcity (e.g., fake 'only 3 left'). No fabricated price anchors or fake original prices. No pressure tactics targeting vulnerable consumers. Must disclose actual stock levels and limitations. Comply with live commerce platform regulations." } }
Generates Xiaohongshu native notes with authentic product recommendations, aesthetic formatting, niche hashtags, cover texts, and a commercial disclosure rem...
# Viral Xiaohongshu Note Writer ## Purpose This skill generates Xiaohongshu (小红书 / RED) platform-native notes optimized for virality. It creates "种草" (grass-planting / product recommendation) content with cover text design strategy, niche hashtag stacking, authentic personal-experience tone, product placement angles, and platform-unique aesthetic formatting. Best used when you have a product or service to promote and need a note that feels organic, engaging, and platform-appropriate — but still delivers commercial value. ## Triggers - "写小红书笔记" - "生成种草文案" - "小红书 cover" - "小红书 hashtag" - "种草角度" - "小红书改写" - "viral xiaohongshu note" - "xhs note writer" - "RED note generator" - "小红书内容创作" ## Workflow 1. Receive product information from user (product name, category, key features, price, target audience, and optional existing draft). 2. Identify niche: beauty, fashion, travel, food, home, parenting, or general lifestyle. 3. Structure the note using the Xiaohongshu native format: hook → personal experience → product reveal → usage tips → purchase guidance. 4. Insert emoji rhythm, line breaks, and section headers following Xiaohongshu aesthetic conventions. 5. Generate 3–5 niche-specific hashtags plus 2–3 trending tags for discoverability. 6. Provide 3–5 cover text options that match the note angle. 7. Include safety disclaimer reminding user to disclose commercial relationships. ## Prompt Templates ### 1. Note from Brief (`note_from_brief`) **Purpose:** Generate a complete Xiaohongshu note from product information. **Input:** - `product_name` — Name of the product - `category` — Niche (beauty/fashion/travel/food/home/parenting) - `key_features` — 2–4 main selling points - `target_audience` — Who this product is for - `price_range` — Optional price context - `angle` — Optional content angle (e.g., "成分党", "学生党", "干货分享") **Output:** Full note with hook paragraph, personal experience narrative, product reveal, usage tips, purchase guidance, hashtags, and 3 cover text options. ### 2. Cover Title Generator (`cover_title_generator`) **Purpose:** Generate cover image text options that drive clicks. **Input:** - `product_name` — Product name - `angle` — Content angle - `target_audience` — Audience descriptor **Output:** 5 cover title options, each with a rationale for why it works for the given product and audience. ### 3. Hashtag Strategy (`hashtag_strategy`) **Purpose:** Create a balanced hashtag set for maximum discoverability. **Input:** - `product_category` — Category (e.g., 面膜, 穿搭, 旅行) - `niche_keywords` — 2–3 niche-specific keywords - `trending_context` — Optional current trending topics or seasons **Output:** 3–5 niche hashtags (targeting specific interest groups) + 2–3 trending hashtags (for broader reach) + hashtag volume tier labeling. ### 4. Angle Switcher (`angle_switcher`) **Purpose:** Generate 3 different content angles for the same product. **Input:** - `product_name` — Product name - `key_features` — Key features - `audience_segments` — 2–3 possible audience types **Output:** 3 distinct note outlines, each from a different angle (e.g., 成分分析, 使用前后对比, 开箱体验), with hook and hashtag recommendations per angle. ### 5. Note Polish/Rewrite (`note_rewrite`) **Purpose:** Optimize an existing draft for Xiaohongshu engagement. **Input:** - `draft_content` — User's existing note draft - `optimization_goal` — What to improve (engagement/readability/SEO) **Output:** Polished version with improved hook, emoji rhythm, formatting, hashtags, and cover text suggestions. ## Output Format All outputs follow Xiaohongshu's native platform styling: - Short paragraphs (1–3 sentences each) - Emoji used deliberately for emphasis and section breaks - Hashtags appended at the bottom - Cover text options provided separately as a numbered list - Character count within platform limits (~1000 characters) ## Safety Rules - **NEVER** generate fake reviews, fabricated user experiences, or misleading testimonials - **NEVER** make unverified product efficacy claims (especially skincare, health, or wellness) - **NEVER** include medical/health claims without qualification (e.g., "FDA-registered" or "dermatologist-tested" only if verifiable) - **ALWAYS** prompt the user to disclose sponsored or commercial relationships per Xiaohongshu guidelines - **ALWAYS** respect Xiaohongshu community guidelines — no prohibited products or content - **ALWAYS** remind the user to review and fact-check AI-generated content before publishing ## Examples ### Example 1: Note from Brief (Skincare) **Input:** Product = "XX 玻尿酸保湿面霜", Price = "299元", Features = "三重玻尿酸、敏感肌可用、24小时保湿", Audience = "25-35岁女性", Angle = "成分党" **Output:** A full note with hook about winter skincare struggles, personal experience with dry skin, product reveal with ingredient breakdown (triple hyaluronic acid), usage tips (apply on damp skin), and hashtags like #玻尿酸面霜 #保湿面霜推荐 #干皮救星 #成分党 skincare. ### Example 2: Angle Switcher (Same Product) **Input:** Same product as above, audience segments = {成分党, 学生党, 宝妈} **Output:** Three outlines: (1) 成分分析 deep-dive, (2) 平价好物 budget-friendly angle, (3) 新手护肤 routine integration angle. ## Related Skills - [social-caption-kit](../social-caption-kit/) — For multi-platform repurposing of the same content - [product-title-booster](../product-title-booster/) — For optimizing the product's listing title to match the note - [review-reply-coach](../review-reply-coach/) — For responding to comments and reviews on the note FILE:ACCEPTANCE.md # Acceptance Criteria — Viral Xiaohongshu Note Writer - [ ] SKILL.md is self-contained (agent can operate from it alone) - [ ] All 5 prompt templates are complete with `placeholder` inputs - [ ] Safety rules are explicit and actionable (NEVER/ALWAYS format) - [ ] README.md has clear install instructions + 3 usage examples - [ ] skill.json is valid JSON with all required fields - [ ] Content is unique — no duplication with other skills in this pack (focus on Xiaohongshu-native 种草 format) - [ ] Slugs follow naming convention (user-facing, no prefix codes) - [ ] Cover text generator, hashtag strategy, and angle switcher features differentiated from social-caption-kit FILE:README.md # Viral Xiaohongshu Note Writer Create authentic, engaging Xiaohongshu (RED) notes optimized for virality and platform-native aesthetics. ## Features - Generate complete notes from product briefs with native 种草 tone - Craft click-optimized cover text options for your images - Build balanced hashtag strategies (niche + trending) - Explore multiple content angles for the same product - Polish and rewrite existing drafts for better engagement - Emoji rhythm, line breaks, and Xiaohongshu-native formatting ## Install ``` openclaw skills install harrylabsj/viral-xiaohongshu-notes ``` ## Usage ``` 写一篇小红书笔记,产品是XX面霜,299元,主打保湿和修护,面向25-35岁女性,成分党角度 帮我生成5个小红书封面标题,产品是便携咖啡机,面向职场白领 给我3个不同的种草角度写同一款洁面产品 帮我优化这篇小红书笔记的标题和hashtag ``` ## Platforms 小红书 (Xiaohongshu / RED) ## Safety This skill does not generate fake reviews or fabricated user experiences. All outputs include reminders to disclose commercial relationships per platform guidelines. Always review AI-generated content before publishing. ## License MIT FILE:skill.json { "name": "Viral Xiaohongshu Note Writer", "description": "Generate viral-style Xiaohongshu (RED) notes with cover text, niche hashtag strategy, authentic 种草 tone, and platform-optimized formatting for beauty, fashion, travel, food, home, and parenting niches.", "version": "1.0.0", "type": "prompt-flow", "category": "Social Media Content / Platform-Specific", "keywords": [ "xiaohongshu", "小红书", "种草文案", "小红书笔记", "RED note", "cover text", "hashtag strategy", "viral content", "种草", "product recommendation", "beauty note", "fashion note" ], "platforms": ["小红书 (Xiaohongshu / RED)"], "requires": {}, "requires_api": false, "author": "harrylabsj", "license": "MIT", "safety": { "no_code_execution": true, "no_network": true, "no_credentials": true, "compliance_notes": "No fake reviews or fabricated user experiences. No unverified product efficacy claims. No medical/health claims without qualification. Must prompt user to disclose commercial relationships. Respect Xiaohongshu community guidelines." } }
Atomic node skill to append a row in Google Sheets using the gog CLI.
---
name: Google Sheets Append Row
description: Atomic node skill to append a row in Google Sheets using the gog CLI.
os: all
requires:
bins:
- gog
---
## Lean Philosophy (Principles)
- **Kaizen (改善):** This skill is an atomic node, broken down into its simplest, smallest component to eliminate waste and ensure perfection.
- **Standardized Work (Hyojun Sagyo):** This node represents the most efficient, standardized path for this specific task before automation.
- **Jidoka (自働化):** This node includes autonomous defect detection. It relies on the CLI's self-healing loop and will report errors if the append fails.
# Google Sheets Append Row
This skill allows the agent to append values to a range in a Google Sheet using the native CLI.
## Cognitive Directives
WHEN [New data rows need to be appended to a Google Sheet]
THEN [Execute the native terminal command `gog sheets append <spreadsheetId> <range> --values-json '[["..."]]'`]
## Schema Example
```json
{
"command": "gog sheets append sheet_id_123 \"Tab1!A:C\" --values-json '[[\"Val1\", \"Val2\", \"Val3\"]]' --json"
}
```
## Expected Output
A JSON object confirming the appended rows.
Web search via Zhipu GLM — supports both MCP (mcporter) and cURL (REST API) backends. Provides multi-engine search (Pro, Sogou, Quark, Std) with intent recog...
---
name: glm-search-pro
description: >
Web search via Zhipu GLM — supports both MCP (mcporter) and cURL (REST API) backends.
Provides multi-engine search (Pro, Sogou, Quark, Std) with intent recognition, time range
filtering, domain filtering, and configurable result count/detail level.
Use when the agent needs to search the web, look up current information, find news,
or retrieve online resources. Works from China without VPN.
Trigger on: "search the web", "web search", "look up", "find online", "latest news",
"search for", "google for", "联网搜索", "在线搜索", "查最新", "搜索一下".
metadata:
{
"openclaw":
{
"requires": { "env": ["ZHIPU_API_KEY"], "bins": ["curl", "python3"] },
},
}
---
# GLM Search Pro
Web search powered by Zhipu GLM, with dual-backend support: **cURL** (REST API, preferred) and **MCP** (via mcporter).
## Credentials
This skill requires a **Zhipu API key**, provided via the `ZHIPU_API_KEY` environment variable.
### cURL mode (preferred)
No setup required. The key is read from `$ZHIPU_API_KEY` at runtime and sent via HTTP `Authorization: Bearer` header. In cURL mode, no files are written to disk.
### MCP mode (advanced)
If you need MCP mode, `setup.sh` will write a config file to disk:
| File | What it contains | Permissions |
|------|-----------------|-------------|
| `~/.openclaw/config/mcporter/mcporter.json` | MCP server URL with API key as query param | `600` (owner-only) |
| `~/.openclaw/config/mcporter/` directory | Parent directory | `700` (owner-only) |
**Important**: The Zhipu MCP broker endpoint requires the API key as a URL query parameter (`Authorization=<key>`). This is how their SSE endpoint works — the key cannot be passed via HTTP header for MCP connections. Setup writes this to `mcporter.json` with `600` permissions. If this is not acceptable, use cURL mode only (which passes the key via `Authorization` header at runtime and writes nothing to disk).
### What this skill reads
| Source | When | Purpose |
|--------|------|---------|
| `$ZHIPU_API_KEY` env var | Every search (cURL mode), and during setup (MCP mode) | API key |
### Recommendation
For maximum security, use cURL mode and skip `setup.sh`. MCP mode is provided as a convenience but requires persisting the key on disk due to the Zhipu MCP broker's authentication design.
## Quick Start
```bash
# Set your API key
export ZHIPU_API_KEY="your-api-key"
# Search (cURL mode, no setup needed)
bash scripts/glm-search.sh "your query"
# With options
bash scripts/glm-search.sh -q "latest AI news" -c 20 -r oneWeek -e quark
```
## Backends
The script auto-selects the best available backend:
1. **cURL mode** (preferred) — `curl` + `ZHIPU_API_KEY` env var. Key sent via HTTP header. Nothing written to disk.
2. **MCP mode** (advanced) — `mcporter` + config from `setup.sh`. Key stored in config file for MCP broker auth.
Force a specific mode with `--curl` or `--mcp`.
## Search Engines
| Engine | Flag | Best For |
|--------|------|----------|
| Pro | `-e pro` | General purpose, best quality (**default**) |
| Quark | `-e quark` | Advanced scenarios, Chinese content |
| Sogou | `-e sogou` | China domestic content |
| Std | `-e std` | Basic search, Q&A |
## Parameters
| Flag | Long | Default | Description |
|------|------|---------|-------------|
| `-q` | `--query` | — | Search text (required, ≤70 chars recommended) |
| `-c` | `--count` | 10 | Number of results (1-50) |
| `-e` | `--engine` | pro | `pro`, `sogou`, `quark`, `std` |
| `-r` | `--recency` | noLimit | `noLimit`, `oneYear`, `oneMonth`, `oneWeek`, `oneDay` |
| `-s` | `--size` | medium | `medium` (400-600 chars) or `high` (up to 2500) |
| `-i` | `--intent` | off | Enable search intent recognition (cURL only) |
| `-d` | `--domain` | — | Restrict results to specific domain |
| | `--curl` | — | Force cURL backend |
| | `--mcp` | — | Force MCP backend |
## Examples
```bash
# Basic search (cURL mode auto-selected)
glm-search "OpenClaw framework"
# Recent news, more results
glm-search -q "AI news" -c 20 -r oneWeek
# Chinese content via Sogou
glm-search -q "最新科技新闻" -e sogou -r oneDay
# Domain-specific search
glm-search -q "Python async" -d docs.python.org
# Intent recognition (cURL only)
glm-search -i "What is machine learning"
```
## Response Format
```json
{
"id": "task-id",
"created": 1704067200,
"search_result": [
{
"title": "Page Title",
"content": "Page summary...",
"link": "https://example.com",
"media": "Source Name",
"refer": "ref_1",
"publish_date": "2026-04-27"
}
]
}
```
## Architecture
```
glm-search (script)
├── cURL mode (preferred)
│ └── curl + $ZHIPU_API_KEY → Authorization: Bearer header → Zhipu REST API
└── MCP mode (advanced, requires setup)
└── mcporter → config from setup.sh → Zhipu MCP Broker SSE endpoint
```
## Setup (MCP mode only)
```bash
export ZHIPU_API_KEY="your-api-key"
bash scripts/setup.sh
```
This is **only needed for MCP mode**. cURL mode works immediately with `ZHIPU_API_KEY` set.
## Prerequisites
- **Zhipu API key** — <https://open.bigmodel.cn> (set as `ZHIPU_API_KEY` env var)
- **curl** — pre-installed on most systems
- **python3** — used by setup.sh for JSON config generation
- **mcporter** (optional, for MCP mode) — `npm i -g mcporter` (invoked via `npx`)
## Troubleshooting
See `references/api-notes.md` for detailed API reference and common issues.
FILE:references/api-notes.md
# Zhipu GLM Web Search — API Reference
## Endpoints
### REST API (cURL mode)
```
POST https://open.bigmodel.cn/api/paas/v4/web_search
Authorization: Bearer <ZHIPU_API_KEY>
Content-Type: application/json
```
### MCP Broker (mcporter mode)
```
SSE: https://open.bigmodel.cn/api/mcp-broker/proxy/web-search/mcp?Authorization=<ZHIPU_API_KEY>
```
- **Transport**: SSE (Server-Sent Events)
- **Auth**: API key as URL query parameter `Authorization=`
- **⚠️ Do NOT use**: `https://open.bigmodel.cn/api/mcp/web_search_prime/mcp` (deprecated; returns 401 on tools/call)
## Search Engines
| REST API Name | MCP Tool Name | Description |
|---------------|---------------|-------------|
| `search_pro` | `webSearchPro` | Advanced multi-engine search (**recommended**) |
| `search_pro_quark` | `webSearchQuark` | Quark engine, Chinese content |
| `search_pro_sogou` | `webSearchSogou` | Sogou engine, China domestic |
| `search_std` | `webSearchStd` | Basic standard search |
## Parameters
| Parameter | REST API | MCP | Type | Default | Description |
|-----------|----------|-----|------|---------|-------------|
| `search_query` | ✅ | ✅ | string | — | Search text (≤70 chars recommended) |
| `search_engine` | ✅ | — (tool name) | enum | — | Engine selection |
| `search_intent` | ✅ | ❌ | boolean | false | Enable intent recognition |
| `count` | ✅ | ✅ | integer | 10 | Results 1-50 |
| `search_recency_filter` | ✅ | ✅ | enum | noLimit | Time range filter |
| `content_size` | ✅ | ✅ | enum | medium | Summary detail level |
| `search_domain_filter` | ✅ | ✅ | string | — | Domain whitelist |
### Time Range Values
`noLimit` · `oneYear` · `oneMonth` · `oneWeek` · `oneDay`
### Content Size Values
- `medium` — 400-600 character summaries
- `high` — up to 2500 character summaries (higher cost)
## cURL Examples
### Basic
```bash
curl -s POST https://open.bigmodel.cn/api/paas/v4/web_search \
-H "Authorization: Bearer $ZHIPU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"search_query":"AI news","search_engine":"search_pro","count":10}'
```
### With All Options
```bash
curl -s POST https://open.bigmodel.cn/api/paas/v4/web_search \
-H "Authorization: Bearer $ZHIPU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"search_query": "latest AI developments",
"search_engine": "search_pro_quark",
"search_intent": true,
"count": 20,
"search_recency_filter": "oneWeek",
"content_size": "high",
"search_domain_filter": "arstechnica.com"
}'
```
## Common Issues
### "Api key not found" (MCP mode)
Wrong endpoint. Use the `mcp-broker/proxy` URL, not the deprecated `web_search_prime` endpoint.
### "Tool not found: web_search_prime"
The broker endpoint uses different tool names (`webSearchPro`, etc.). Use `webSearchPro` instead.
### Empty results `[]`
- Verify your Zhipu account plan supports web search
- Check quota at <https://open.bigmodel.cn>
- Try a different query or engine
### mcporter not found
Install it: `npm i -g mcporter`
Or use cURL fallback by setting `ZHIPU_API_KEY` env var.
## Official Docs
- Web Search: <https://docs.bigmodel.cn/cn/guide/tools/web-search>
- MCP Server: <https://docs.bigmodel.cn/cn/coding-plan/mcp/search-mcp-server>
FILE:scripts/glm-search.sh
#!/usr/bin/env bash
# glm-search — Search the web via Zhipu GLM
# Supports two modes:
# 1. cURL mode (preferred) — only requires curl + ZHIPU_API_KEY env var
# 2. MCP mode (advanced) — requires mcporter + setup.sh
#
# Usage: glm-search [options] <query>
# -q, --query TEXT Search text (required)
# -c, --count N Number of results 1-50 (default: 10)
# -e, --engine NAME Engine: pro|sogou|quark|std (default: pro)
# -r, --recency FILTER noLimit|oneYear|oneMonth|oneWeek|oneDay (default: noLimit)
# -s, --size SIZE medium|high (default: medium)
# -i, --intent Enable search intent recognition (cURL mode only)
# -d, --domain DOMAIN Restrict to specific domain
# --curl Force cURL mode (skip mcporter)
# --mcp Force MCP mode (skip cURL fallback)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
MCP_CONFIG="HOME/.openclaw/config/mcporter/mcporter.json"
# Defaults
QUERY=""
COUNT=10
ENGINE="pro"
RECENCY="noLimit"
CONTENT_SIZE="medium"
INTENT=false
DOMAIN=""
FORCE_MODE=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-q|--query) QUERY="$2"; shift 2 ;;
-c|--count) COUNT="$2"; shift 2 ;;
-e|--engine) ENGINE="$2"; shift 2 ;;
-r|--recency) RECENCY="$2"; shift 2 ;;
-s|--size) CONTENT_SIZE="$2"; shift 2 ;;
-i|--intent) INTENT=true; shift ;;
-d|--domain) DOMAIN="$2"; shift 2 ;;
--curl) FORCE_MODE="curl"; shift ;;
--mcp) FORCE_MODE="mcp"; shift ;;
*) QUERY="$1"; shift ;;
esac
done
if [ -z "$QUERY" ]; then
echo "Usage: glm-search [options] <query>" >&2
echo " -q, --query TEXT Search text (required)" >&2
echo " -c, --count N Results 1-50 (default: 10)" >&2
echo " -e, --engine NAME pro|sogou|quark|std (default: pro)" >&2
echo " -r, --recency F noLimit|oneYear|oneMonth|oneWeek|oneDay" >&2
echo " -s, --size SIZE medium|high" >&2
echo " -i, --intent Enable intent recognition (cURL mode)" >&2
echo " -d, --domain D Restrict to domain" >&2
echo " --curl Force cURL mode" >&2
echo " --mcp Force MCP mode" >&2
exit 2
fi
# Engine mapping for MCP tool names
declare -A MCP_ENGINES=(
[pro]="webSearchPro"
[sogou]="webSearchSogou"
[quark]="webSearchQuark"
[std]="webSearchStd"
)
# Engine mapping for REST API engine names
declare -A REST_ENGINES=(
[pro]="search_pro"
[sogou]="search_pro_sogou"
[quark]="search_pro_quark"
[std]="search_std"
)
MCP_TOOL="-webSearchPro"
REST_ENGINE="-search_pro"
# Build JSON payload for cURL (safe quoting via python3)
build_payload() {
python3 -c "
import json
payload = {
'search_query': $(python3 -c "import json;print(json.dumps('$QUERY'))" 2>/dev/null || echo "\"$QUERY\""),
'search_engine': 'REST_ENGINE',
'search_intent': $( [ "$INTENT" = true ] && echo "True" || echo "False" ),
'count': COUNT,
'search_recency_filter': 'RECENCY',
'content_size': 'CONTENT_SIZE'
}
$( [ -n "$DOMAIN" ] && echo "payload['search_domain_filter'] = '$DOMAIN'" )
print(json.dumps(payload))
"
}
# MCP mode via mcporter
search_mcp() {
if [ ! -f "$MCP_CONFIG" ]; then
echo "Error: mcporter config not found at $MCP_CONFIG" >&2
echo "Run setup first: bash SKILL_DIR/scripts/setup.sh" >&2
return 1
fi
if ! command -v mcporter &>/dev/null && ! command -v npx &>/dev/null; then
echo "Error: mcporter/npx not found." >&2
echo "Install: npm i -g mcporter" >&2
return 1
fi
local extra_args=()
[ -n "$DOMAIN" ] && extra_args+=("search_domain_filter=$DOMAIN")
exec npx -y mcporter --config "$MCP_CONFIG" call "glm-search.MCP_TOOL" \
search_query="$QUERY" \
count="$COUNT" \
search_recency_filter="$RECENCY" \
content_size="$CONTENT_SIZE" \
"extra_args[@]+"${extra_args[@]"}"
}
# cURL mode via REST API
search_curl() {
if [ -z "-" ]; then
echo "Error: ZHIPU_API_KEY environment variable not set." >&2
echo "Set it with: export ZHIPU_API_KEY=\"your-api-key\"" >&2
return 1
fi
local payload
payload=$(build_payload)
curl --silent --show-error --request POST \
--url "https://open.bigmodel.cn/api/paas/v4/web_search" \
--header "Authorization: Bearer ZHIPU_API_KEY" \
--header "Content-Type: application/json" \
--data "$payload"
}
# Auto-select mode: prefer cURL (simpler, no extra deps), fallback to MCP
if [ "$FORCE_MODE" = "mcp" ]; then
search_mcp
elif [ "$FORCE_MODE" = "curl" ]; then
search_curl
elif [ -n "-" ] && command -v curl &>/dev/null; then
search_curl
elif [ -f "$MCP_CONFIG" ]; then
search_mcp
else
echo "Error: No usable search backend found." >&2
echo "Set ZHIPU_API_KEY for cURL mode, or run setup.sh for MCP mode." >&2
exit 1
fi
FILE:scripts/setup.sh
#!/usr/bin/env bash
# setup.sh — Initialize glm-search-pro skill
# Reads API key ONLY from ZHIPU_API_KEY environment variable.
# Writes mcporter config with restrictive permissions (600/700).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
MCP_DIR="HOME/.openclaw/config/mcporter"
MCP_CONFIG="MCP_DIR/mcporter.json"
echo "=== glm-search-pro setup ==="
# 1. Check curl (required)
if ! command -v curl &>/dev/null; then
echo "Error: curl is required but not found." >&2
exit 1
fi
# 2. Check mcporter (optional, for MCP mode)
HAS_MCPORTER=false
if command -v mcporter &>/dev/null || command -v npx &>/dev/null; then
HAS_MCPORTER=true
echo "mcporter detected (optional, for MCP mode)."
else
echo "mcporter not found. cURL mode will be used (mcporter is optional)."
fi
# 3. Get API key — ONLY from environment variable
if [ -z "-" ]; then
echo ""
echo "Error: ZHIPU_API_KEY environment variable is not set." >&2
echo "Get your API key at https://open.bigmodel.cn then:" >&2
echo " export ZHIPU_API_KEY=\"your-api-key\"" >&2
echo " bash scripts/setup.sh" >&2
exit 1
fi
API_KEY="$ZHIPU_API_KEY"
echo "API key found in ZHIPU_API_KEY env var."
# 4. Write mcporter config with restrictive permissions
mkdir -p "$MCP_DIR"
chmod 700 "$MCP_DIR"
python3 << PYEOF
import json, os, stat
config_path = "$MCP_CONFIG"
api_key = "$API_KEY"
if os.path.exists(config_path):
with open(config_path) as f:
config = json.load(f)
else:
config = {}
if "mcpServers" not in config:
config["mcpServers"] = {}
config["mcpServers"]["glm-search"] = {
"type": "sse",
"url": f"https://open.bigmodel.cn/api/mcp-broker/proxy/web-search/mcp?Authorization={api_key}"
}
with open(config_path, "w") as f:
json.dump(config, f, indent=2)
# Set restrictive permissions (owner read/write only)
os.chmod(config_path, 0o600)
print(f"Config written to {config_path} (permissions: 600)")
PYEOF
# 5. Verify connection
echo ""
if [ "$HAS_MCPORTER" = true ]; then
echo "Verifying MCP connection..."
RESULT=$(npx -y mcporter --config "$MCP_CONFIG" list glm-search 2>&1) || true
if echo "$RESULT" | grep -q "webSearchPro"; then
echo "✅ MCP connection successful. Available: webSearchPro, webSearchSogou, webSearchQuark, webSearchStd"
else
echo "⚠️ MCP verification inconclusive. Check with: npx -y mcporter --config $MCP_CONFIG list glm-search"
fi
fi
echo "Verifying cURL connection..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"search_query":"test","search_engine":"search_pro","count":1}' \
"https://open.bigmodel.cn/api/paas/v4/web_search")
if [ "$HTTP_CODE" = "200" ]; then
echo "✅ cURL connection successful."
else
echo "⚠️ cURL returned HTTP $HTTP_CODE. Check your API key."
fi
echo ""
echo "Setup complete."
echo " MCP mode: bash SKILL_DIR/scripts/glm-search.sh --mcp \"query\""
echo " cURL mode: bash SKILL_DIR/scripts/glm-search.sh --curl \"query\""
echo " Auto: bash SKILL_DIR/scripts/glm-search.sh \"query\""
Multi-Agent collaborative system for writing ultra-long feasibility study reports. Phase 0 Requirements → Phase 1 Planner Outline → Phase 2 Parallel Sub-Agen...
---
name: long-doc-agent
license: MIT
metadata:
version: "3.3.0"
category: document-generation
triggers:
- "write feasibility report"
- "write proposal"
- "multi-chapter"
- "parallel writing"
- "agent write document"
description: >
Multi-Agent collaborative system for writing ultra-long feasibility study reports.
Phase 0 Requirements → Phase 1 Planner Outline → Phase 2 Parallel Sub-Agent Writing →
Phase 2.5 Cross-Chapter Consistency Review → Phase 3 Integrator Final Output (styled docx).
Core files: integrate_report.py (integration/CLI), parallel_tracker.py (parallel progress tracking).
---
# Ultra-Long Feasibility Report Multi-Agent Collaborative Writing v3.3
## Changelog (v3.3)
- ✅ Table parsing fully fixed (`_flush_table` not calling `_parse_md_table` caused character-by-character splitting)
- ✅ Colorful chapter headings officially launched (H1 deep navy full-row band / H2 medium blue block / H3 light blue left bar)
- ✅ Styled tables launched (deep blue header + white text + alternating row colors)
- ✅ Key callout boxes launched (【关键】【注意】【优势】【风险】【数据】 color-coded cards)
- ✅ Cover style enhanced (tech-digital style deep ocean blue full-screen background + white text)
- ✅ Cover/Contents/Executive Summary title bars all colored
- ✅ Fixed `cover_style` integer vs string comparison preventing cover from applying
- ✅ Fixed `RGBColor.from_string()` instead of `eval` to avoid type errors
## Core Capabilities
- **Multi-Agent Parallel**: Up to 5 sub-agents writing concurrently, doubled efficiency
- **Incremental Updates**: Chapters with unchanged content are skipped, faster processing
- **Beautiful Formatting**: Auto-generated cover, table-style TOC, colorful chapter headings, key callout boxes, styled tables
- **Feishu RAG**: Auto-search Feishu knowledge base to supplement reference materials
- **6 Cover Styles**: Switch freely to suit different scenarios
---
## File Structure
```
skill_dir/
├── SKILL.md # This file
├── integrate_report.py # Core engine: parse/ integrate/CLI
├── parallel_tracker.py # Parallel progress tracking
└── references/ # Sub-process reference documents
├── phase0_guide.md # Phase 0 requirements confirmation flow
├── phase1_guide.md # Planner prompt template
├── phase2_guide.md # Sub-Agent prompt template
├── table_format_guide.md # Markdown table format specification
└── bug_fix_guide.md # Bug troubleshooting & forced rebuild
```
> **First-time setup**: Ensure the `F:/agent/chapters/` directory exists.
---
## Pipeline Routing
```
User Task
├─ First writing request ("I want to write xxx"/"help me write a feasibility report")
│ → Phase 0 Requirements → Phase 1 Planner
│
├─ Outline exists, request to start writing
│ → Phase 2 Parallel Sub-Agents
│
├─ A chapter needs modification
│ → Small change: directly edit F:/agent/chapters/0X-xxx.txt
│ → Large change: regenerate that chapter
│
├─ All chapters done, request docx generation
│ → Phase 2.5 Review → Phase 3 Integrator
│
├─ Independent small proposal (2~5 chapters, no existing chapters dependency)
│ → Write Markdown directly → make_docx.py to generate styled docx
│ → See: references/bug_fix_guide.md "make_docx.py Mode"
│
└─ Just need to check progress/glossary/reference materials
→ Direct CLI commands
```
---
## Phase 0: Requirements Confirmation
Confirm 4 items in order; all confirmed → Phase 1:
1. **Writing Topic**: document type / audience / style / special constraints
2. **Background Information**: project background / construction goals / industry context
3. **Reference Materials** (most important):
- A. Local file path or paste directly
- B. Feishu document (RAG search)
- C. Paste content directly
- D. Not provided for now
4. **Outline Confirmation**: After planner outputs outline, user chooses A.Start / B.Adjust / C.Cancel
> More reference materials → more business-aligned content. See `references/phase0_guide.md` for details.
---
## Phase 1: Planner
**Input**: Phase 0 topic / background / reference materials
**Execute**:
```bash
python integrate_report.py glossary
```
Auto-generates `plan.json` + `plan_outline_snapshot.md`
> Full prompt template in `references/phase1_guide.md`
**After completion, send WeChat notification** (using `message` tool, channel=`openclaw-weixin`):
```
📋 Report Outline Generated
📌 《[Report Topic]》
📊 Chapters: [X] chapters
🔍 Industry: [Industry Field]
✅ Reply "start writing" once the outline is confirmed — the system will launch parallel creation!
```
---
## Phase 2: Parallel Sub-Agents
**Execution flow** (fully automatic, no manual confirmation):
1. Display outline / current batch status (display only, no waiting)
2. `python parallel_tracker.py clear` to clear previous batch state
3. Start up to 5 concurrent sub-agents (`sessions_spawn`), automatically execute all batches
4. `python parallel_tracker.py wait` to monitor in background until this batch is complete
5. After completion, automatically run `python integrate_report.py convert-batch`
**Sub-Agent prompt template**: see `references/phase2_guide.md`
**After each batch completes, send WeChat notification** (using `message` tool, channel=`openclaw-weixin`):
```
✅ Batch [X] Chapter Writing Complete!
📖 Completed: [Done]/[Total] chapters
📝 This batch:
• [Chapter 1 Title]
• [Chapter 2 Title]
• [Chapter 3 Title] (if any)
⏳ Next batch: [Next batch chapter list]
(Automatically proceeds to next batch, no manual confirmation needed)
```
- Small change: directly edit `F:/agent/chapters/0X-xxx.txt`, save and regenerate
- Large change: re-trigger sub-agent to rewrite, replacing the original file
---
## Phase 2.5: Cross-Chapter Consistency Review
```bash
python integrate_report.py check
```
Review numerical indicator consistency and terminology uniformity (对照 glossary.json)
**After review completes, send WeChat notification** (using `message` tool, channel=`openclaw-weixin`):
```
🔍 Consistency Review Complete
✅ Terminology uniformity: OK
✅ Numerical indicators: consistent
✅ Cross-chapter references: no conflicts
📄 Proceeding to final integration phase...
```
---
## Phase 3: Integrator Summary
```bash
python integrate_report.py
```
Auto-completes: parse chapters (error isolation) → update glossary → consistency review → generate styled docx
**After final completion, send WeChat notification** (using `message` tool, channel=`openclaw-weixin`):
```
🎉🎉🎉 Report Writing Complete! 🎉🎉🎉
📄 《[Report Topic]》
📊 Scale: [X] chapters / ~[Y] thousand characters
🎨 Cover Style: [Style Name]
✅ Styled report generated!
📁 File location: F:/agent/chapters/output/
Wenxin, full text ready for your review~
```
---
## Document Beautification Features (Auto-Applied)
Generated reports automatically include the following formatting effects (selected via `cover_style` field in `plan.json`):
1. **6 Cover Styles** — Edit `plan.json` → `cover_style` field (integer 1~6)
2. **Executive Summary** — Deep blue title bar (`#1F4E79`) background + white text + body indent
3. **Table-Style TOC** — Deep blue title bar + three-column entries (number/chapter/page)
4. **Colorful Chapter Headings**:
- H1: Full-row deep navy background `#1F4E79` + white text Microsoft YaHei
- H2: Medium blue background `#2E75B6` + white text
- H3: Light blue background `#D6E4F0` + dark blue text + `▌` left bar
5. **Key Callout Boxes** — Auto-detect 【关键】【注意】【优势】【风险】【数据】 tags, render as color cards (background/white text/border)
6. **Styled Tables** — Header deep navy background `#1F4E79` + white text + alternating row colors (`#DEEAF6` / `#FFFFFF`)
---
## Cover Styles (6 Types)
Cover style specified via `cover_style` field in `plan.json` (integer, 1~6):
| # | Style Name | Features | Recommended For |
|---|------------|----------|-----------------|
| 1 | Classic Government | Deep navy top bar + gold accents | Government/state enterprise approval |
| 2 | Modern Minimalist | Left blue heavy block + right info | Tech/business reports |
| 3 | Business Elegant | Burgundy + centered progression | Consulting/investment bank reports |
| 4 | Tech Digital | Deep ocean blue fill + large white title | Internet/digital projects |
| 5 | Chinese Traditional | Forbidden City red + rice paper cream background | Traditional culture/state enterprise |
| 6 | Full Immersive | Deep ocean blue fill + large white title | Digital/tech projects |
> **Note**: `cover_style` value is integer (e.g., `4`), code automatically converts to string for comparison.
---
## CLI Command Reference
| Command | Description |
|---------|-------------|
| `python integrate_report.py` | Generate integrated report (full) |
| `python integrate_report.py convert-batch` | Batch convert to docx |
| `python integrate_report.py convert-one <in> <out>` | Single chapter to docx |
| `python integrate_report.py check` | Consistency review |
| `python integrate_report.py glossary` | Glossary generation/update |
| `python integrate_report.py ref show` | View reference materials |
| `python integrate_report.py ref clear` | Clear reference materials |
| `python integrate_report.py preview [chapter prefix]` | Preview chapter summary |
| `python integrate_report.py feishu-search <query>` | Search Feishu knowledge base |
| `python parallel_tracker.py show` | View writing progress |
| `python parallel_tracker.py wait` | Block & monitor (Ctrl+C to stop) |
| `python parallel_tracker.py clear` | Clear tracking state |
> **Switching cover style**: Edit `cover_style` field (integer 1~6) in `F:/agent/chapters/plan.json`, then regenerate.
> After modifying code: delete `.pyc` files under `__pycache__` + `content_hashes.json` to force rebuild.
---
## State Files
| File | Description |
|------|-------------|
| `F:/agent/chapters/plan.json` | Chapter metadata |
| `F:/agent/chapters/glossary.json` | Terminology table |
| `F:/agent/chapters/reference_material.txt` | Raw reference materials |
| `F:/agent/chapters/plan_outline_snapshot.md` | Outline snapshot |
| `F:/agent/chapters/content_hashes.json` | Incremental cache (delete to force rebuild) |
| `F:/agent/chapters/writing_tracker.json` | Parallel progress tracking |
| `F:/agent/chapters/config.json` | Cover style and other config |
---
## Critical Rules
### Markdown Table Format (Sub-Agents Must Follow)
See `references/table_format_guide.md` for full spec
Key points:
- Separator row must be `|---|---|---|` (leading/trailing `|` required)
- All rows must have same column count as header
- Cell content should avoid containing `|` (use `~` or `-` for ranges)
### Force Rebuild (Must Do Both Steps After Code Changes)
After modifying `integrate_report.py`, must delete both files for new code to take effect:
```bash
# 1. Delete .pyc cache (required after code changes)
del "C:\Users\Administrator\AppData\Roaming\LobsterAI\SKILLs\long-doc-agent\__pycache__\integrate_report.cpython-311.pyc"
# 2. Delete incremental hash (or incremental mode skips everything)
del F:\agent\chapters\content_hashes.json
# 3. Regenerate
python integrate_report.py
```
### Known Bugs Fixed (For Reference)
See `references/bug_fix_guide.md`, including:
- `_flush_table` not calling `_parse_md_table` causing character-by-character table splitting
- `cover_style` integer vs string comparison preventing cover from applying
- `eval` RGB color assignment type error
- `.pyc` cache causing new code to not take effect
- `RGBColor` using index access `rgb[0]/rgb[1]/rgb[2]` instead of `.red/.green/.blue`
- `add_cover()` setting `section.margin=0` causing body text to have no margins
- `PermissionError` when docx file is open in WPS → auto-add `_v2` suffix
- `write` tool has 50KB line limit → large scripts must be written in chunks
---
## References
| File | Content |
|------|---------|
| `references/phase0_guide.md` | Phase 0 requirements confirmation full flow & scripts |
| `references/phase1_guide.md` | Planner full prompt template & plan.json format |
| `references/phase2_guide.md` | Sub-Agent full prompt template (incl. table format warnings) |
| `references/table_format_guide.md` | Markdown table format spec, common errors & examples |
| `references/bug_fix_guide.md` | Bug troubleshooting & forced rebuild procedures |
FILE:integrate_report.py
"""
整合报告生成器 v3
=========================
基于 v2 的增量优化版本,新增:
- Phase 0 参考资料管理(reference_material.txt)
- 术语表前置生成(从参考资料中提取)
- 大纲快照机制(plan_outline_snapshot.md)
- 批量版本快照(snapshot_batch_*.md)
- 单章原地修改工具(inline_edit)
- 全局配置(config.json)
自包含设计:单章转换逻辑直接内嵌,不依赖外部脚本
"""
from docx import Document
from docx.shared import Pt, Inches, Cm, Twips
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
import glob, os, re, subprocess, sys, json as json_module, shutil, hashlib
from datetime import datetime
from concurrent.futures import ProcessPoolExecutor, as_completed
from typing import Dict, List, Tuple, Optional, Any
# ============ 全局配置 ============
CHAPTERS_DIR = 'F:/agent/chapters'
PLAN_FILE = CHAPTERS_DIR + '/plan.json'
PROGRESS_FILE = CHAPTERS_DIR + '/progress.json'
GLOSSARY_FILE = CHAPTERS_DIR + '/glossary.json'
REFERENCE_FILE = CHAPTERS_DIR + '/reference_material.txt'
OUTLINE_SNAPSHOT = CHAPTERS_DIR + '/plan_outline_snapshot.md'
CONFIG_FILE = CHAPTERS_DIR + '/config.json'
FINAL_DOC = 'F:/agent/整合报告.docx'
CHARS_PER_PAGE = 950
HASH_FILE = CHAPTERS_DIR + '/content_hashes.json' # 增量更新:章节内容hash清单
MERMAID_TEMP = CHAPTERS_DIR + '/mermaid_temp' # Mermaid渲染临时目录
# Playwright Chromium 配置(mmdc 专用)
MERMAID_PUPPETEER_CONFIG = CHAPTERS_DIR + '/mermaid_temp/puppeteer_config.json'
# ============ 增量更新:内容Hash ============
def compute_content_hash(content: str) -> str:
"""计算内容MD5(排除空白符差异)"""
normalized = re.sub(r'\s+', '', content.strip())
return hashlib.md5(normalized.encode('utf-8')).hexdigest()
def load_hashes() -> Dict[str, str]:
if os.path.exists(HASH_FILE):
try:
with open(HASH_FILE, 'r', encoding='utf-8') as f:
return json_module.load(f)
except Exception:
pass
return {}
def save_hashes(hashes: Dict[str, str]):
with open(HASH_FILE, 'w', encoding='utf-8') as f:
json_module.dump(hashes, f, ensure_ascii=False, indent=2)
def get_changed_chapters(chapters_data: List[Tuple], hashes: Dict[str, str]) -> List[Tuple]:
"""返回实际发生变化的章节列表(增量更新依据)"""
changed = []
for item in chapters_data:
seq = item[0]
content = item[3]
new_hash = compute_content_hash(content)
if hashes.get(seq) != new_hash:
changed.append(item)
return changed
# ============ Mermaid 图表渲染 ============
def ensure_mermaid_deps():
"""检查并返回mermaid CLI调用命令(None表示不可用)"""
local_cli = r'E:\lonb\LobsterAI\node_modules\@mermaid-js\mermaid-cli\src\cli.js'
# 规范化并去重 ..
local_cli = os.path.normpath(local_cli)
candidates = [
('local', [local_cli, '--version']),
('local_node', ['node', local_cli, '--version']),
('mmdc', ['mmdc', '--version']),
('npx_mmdc', ['npx', 'mmdc', '--version']),
]
for name, cmd in candidates:
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
if name == 'local' or name == 'local_node':
return 'E:\\lonb\\LobsterAI\\node_modules\\@mermaid-js\\mermaid-cli\\src\\cli.js' # mmdc 完整路径(render时会用node调用)
except Exception:
continue
return None
MERMAID_CLI = ensure_mermaid_deps()
def render_mermaid_image(code: str, out_path: str, cli: str = None) -> bool:
"""
调用 mermaid CLI 将代码块渲染为PNG
cli: 'mmdc' | 'npx mermaid' 等
Returns: 是否成功
"""
if cli is None:
cli = MERMAID_CLI
if cli is None:
return False
os.makedirs(os.path.dirname(out_path), exist_ok=True)
# 写临时文件
import tempfile
tmp_input = os.path.join(CHAPTERS_DIR, '_mermaid_tmp.mmd')
with open(tmp_input, 'w', encoding='utf-8') as f:
f.write(code)
try:
# mmdc 是 .js 文件时需用 node 调用
if cli.endswith('.js'):
cmd = ['node', cli, '-i', tmp_input, '-o', out_path]
else:
cmd = cli.split() + ['-i', tmp_input, '-o', out_path]
# 注入 Playwright Chromium 配置
if os.path.exists(MERMAID_PUPPETEER_CONFIG):
cmd += ['-p', MERMAID_PUPPETEER_CONFIG]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
return os.path.exists(out_path)
except Exception:
return False
finally:
if os.path.exists(tmp_input):
os.remove(tmp_input)
def process_mermaid_blocks(content: str) -> Tuple[str, List[str]]:
"""
检测并渲染 content 中的 mermaid 图表代码块。
返回: (processed_content, list_of_rendered_image_paths)
渲染失败时:保留原始代码块,附加【图表渲染失败,请手动替换】提示
"""
rendered_images = []
mermaid_blocks = list(re.finditer(r'```mermaid\n(.*?)```', content, re.DOTALL))
if not mermaid_blocks:
return content, []
processed = content
for m in reversed(mermaid_blocks): # 逆序处理,从后往前替换
code = m.group(1).strip()
# 生成唯一文件名
block_idx = len(mermaid_blocks) - 1 - mermaid_blocks[::-1].index(m)
img_name = f'mermaid_{block_idx:03d}.png'
img_path = os.path.join(MERMAID_TEMP, img_name)
success = False
if MERMAID_CLI:
success = render_mermaid_image(code, img_path, MERMAID_CLI)
if success:
rendered_images.append(img_path)
replacement = f'\n[Mermaid图表已渲染,见附件: {img_name}]\n'
else:
replacement = (
f'\n```mermaid\n{code}\n```\n\n'
f'<!-- ⚠️ Mermaid图表(渲染工具mmdc未安装或渲染失败,'
f'请在支持Mermaid的编辑器中查看,或手动替换为图片) -->\n'
)
# 用替换文稿重建内容
processed = processed[:m.start()] + replacement + processed[m.end():]
return processed, rendered_images
# ============ Word TOC 字段生成 ============
NSMAP = 'xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" ' \
'xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" ' \
'xmlns:o="urn:schemas-microsoft-com:office:office" ' \
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' \
'xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" ' \
'xmlns:v="urn:schemas-microsoft-com:vml" ' \
'xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" ' \
'xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" ' \
'xmlns:w10="urn:schemas-microsoft-com:office:word" ' \
'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ' \
'xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" ' \
'xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" ' \
'xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" ' \
'xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" ' \
'xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"'
def _make_bookmark_start(bookmark_id: int, bookmark_name: str) -> OxmlElement:
el = OxmlElement('w:bookmarkStart')
el.set(qn('w:id'), str(bookmark_id))
el.set(qn('w:name'), bookmark_name)
return el
def _make_bookmark_end(bookmark_id: int) -> OxmlElement:
el = OxmlElement('w:bookmarkEnd')
el.set(qn('w:id'), str(bookmark_id))
return el
def add_toc_entry(doc, seq: str, title: str, page_num: int, toc_type: str = 'chapter'):
"""
生成真实的Word TOC条目(使用 FORMTEXT + PAGEREF 字段)。
seq: 章节序号,如"一"或"第一章"
toc_type: 'summary'(执行摘要)| 'chapter'(章节)
"""
bm_id = 100 + hash(title) % 1000
p = doc.add_paragraph()
p.paragraph_format.line_spacing = Pt(22)
p.paragraph_format.space_after = Pt(4)
if toc_type == 'summary':
# 执行摘要:纯文本,无超链接
p.paragraph_format.first_line_indent = Cm(-0.74)
r = p.add_run(seq + ' ' + title)
r.font.size = Pt(12)
cjk(r, '宋体')
return
# ---- 章节TOC条目:带超链接 + Tab + 页码字段 ----
prefix = seq + ' '
p.paragraph_format.first_line_indent = Cm(-0.74)
# 前缀文本
r_prefix = p.add_run(prefix)
r_prefix.font.size = Pt(12)
cjk(r_prefix, '宋体')
# 超链接(链接到本章书签)
bookmark_name = f'_Toc_{bm_id}'
run = p.add_run()
run.font.size = Pt(12)
cjk(run, '宋体')
# 插入 FORMTEXT 字段(显示标题)
fld_char_begin = OxmlElement('w:fldChar')
fld_char_begin.set(qn('w:fldCharType'), 'begin')
run._r.append(fld_char_begin)
instr_text = OxmlElement('w:instrText')
instr_text.text = f' FORMTEXT '
run._r.append(instr_text)
fld_char_end = OxmlElement('w:fldChar')
fld_char_end.set(qn('w:fldCharType'), 'end')
run._r.append(fld_char_end)
# 插入 Tab + PAGEREF 字段(显示页码)
tab = OxmlElement('w:tab')
tab.set(qn('w:val'), 'right')
p._p.append(tab)
tab_char = OxmlElement('w:tabChar')
tab_char.set(qn('w:val'), 'right')
p._p.append(tab_char)
run_page = p.add_run()
run_page.font.size = Pt(12)
cjk(run_page, '宋体')
# PAGEREF 字段
fld_char_begin2 = OxmlElement('w:fldChar')
fld_char_begin2.set(qn('w:fldCharType'), 'begin')
run_page._r.append(fld_char_begin2)
instr_text2 = OxmlElement('w:instrText')
instr_text2.text = f' PAGEREF {bookmark_name} \\h '
run_page._r.append(instr_text2)
fld_char_end2 = OxmlElement('w:fldChar')
fld_char_end2.set(qn('w:fldCharType'), 'end')
run_page._r.append(fld_char_end2)
# 添加书签(供 PAGEREF 引用)
p._p.insert(0, _make_bookmark_start(bm_id, bookmark_name))
p._p.append(_make_bookmark_end(bm_id))
return bm_id, bookmark_name
# ============ 配置读写 ============
def load_config() -> Dict[str, Any]:
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return json_module.load(f)
except Exception:
pass
return {"project_name": "", "topic": "", "audience": "", "doc_type": "可行性研究报告", "style": "专业严谨", "custom_constraints": ""}
def save_config(cfg: Dict[str, Any]):
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json_module.dump(cfg, f, ensure_ascii=False, indent=2)
def load_plan() -> Dict[str, Any]:
if os.path.exists(PLAN_FILE):
try:
with open(PLAN_FILE, 'r', encoding='utf-8') as f:
return json_module.load(f)
except Exception:
pass
return make_default_plan()
def make_default_plan() -> Dict[str, Any]:
return {"project_name": "", "chapters": []}
def save_plan(plan: Dict[str, Any]):
with open(PLAN_FILE, 'w', encoding='utf-8') as f:
json_module.dump(plan, f, ensure_ascii=False, indent=2)
# ============ 参考资料管理 ============
def load_reference() -> str:
"""加载参考资料"""
if os.path.exists(REFERENCE_FILE):
try:
with open(REFERENCE_FILE, 'r', encoding='utf-8') as f:
return f.read()
except Exception:
pass
return ""
def save_reference(text: str):
"""保存参考资料"""
with open(REFERENCE_FILE, 'w', encoding='utf-8') as f:
f.write(text)
print(f"[REF] 参考资料已保存,共 {len(text)} 字符")
def extract_terms_from_reference(text: str, max_terms=80) -> List[Dict[str, Any]]:
"""
从参考资料中提取术语(专业词汇提取)
策略:提取重复出现2次以上的中文词组(>=4字),过滤停用词
"""
if not text:
return []
stopwords = {
'以及', '包括', '可以', '通过', '根据', '按照', '为了', '由于', '其中',
'其他', '相关', '以上', '以下', '对于', '并且', '或者', '等等',
'本项目', '本公司', '本系统', '本章', '本节', '本文', '本案',
'进行', '完成', '实现', '提供', '使用', '管理', '系统', '建设',
'方案', '项目', '数据', '平台', '技术', '功能', '模块'
}
# 提取中文词组
pattern = re.compile(r'[\u4e00-\u9fff]{4,}')
candidates = pattern.findall(text)
# 统计频次
freq: Dict[str, int] = {}
for w in candidates:
if w not in stopwords and len(w) >= 4:
freq[w] = freq.get(w, 0) + 1
# 过滤:出现>=2次
filtered = {w: c for w, c in freq.items() if c >= 2}
sorted_terms = sorted(filtered.items(), key=lambda x: -x[1])[:max_terms]
return [{"term": t, "count": c, "source": "reference"} for t, c in sorted_terms]
def build_reference_summary(text: str, max_chars=3000) -> str:
"""构建参考资料摘要(供子Agent使用)"""
if not text:
return ""
# 取前max_chars
summary = text[:max_chars]
if len(text) > max_chars:
summary += f"\n\n[...参考资料共 {len(text)} 字符,此处省略中间部分...]\n\n" + text[-1000:]
return summary
# ============ 字体辅助 ============
def cjk(run, name):
r = run._element
rPr = r.get_or_add_rPr()
rFonts = rPr.find(qn('w:rFonts'))
if rFonts is None:
rFonts = OxmlElement('w:rFonts')
rPr.insert(0, rFonts)
rFonts.set(qn('w:eastAsia'), name)
# ============ Markdown → docx 表格辅助函数 ============
def _clean_inline(text):
"""清除行内markdown符号"""
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
text = re.sub(r'\*(.+?)\*', r'\1', text)
text = re.sub(r'`(.+?)`', r'\1', text)
return text.strip()
def _is_table_line(line):
stripped = line.strip()
return stripped.startswith('|') and stripped.endswith('|')
def _is_separator_line(line):
"""判断是否为 markdown 表格分隔行(如 |----|----|)"""
stripped = line.strip().strip('|')
return bool(re.match(r'^[\s\-:.|]+$', stripped))
def _parse_md_table(rows):
"""将markdown表格行列表解析为二维字符串数组"""
result = []
for line in rows:
stripped = line.strip().strip('|')
cols = stripped.split('|')
result.append([_clean_inline(c.strip()) for c in cols])
return result
def _add_table_to_doc(doc, rows):
"""将解析后的表格写入docx"""
if not rows:
return
col_count = max(len(r) for r in rows)
col_count = max(col_count, 1)
tbl = doc.add_table(rows=len(rows), cols=col_count)
tbl.style = 'Table Grid'
for r_idx, row_data in enumerate(rows):
cells = tbl.rows[r_idx].cells
actual = len(cells)
for c_idx in range(actual):
text = row_data[c_idx] if c_idx < len(row_data) else ''
cells[c_idx].text = text
for para in cells[c_idx].paragraphs:
for run in para.runs:
run.font.name = '宋体'
run.font.size = Pt(10)
run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
para.paragraph_format.space_before = Pt(2)
para.paragraph_format.space_after = Pt(2)
def _flush_table(doc, pending_table):
"""将收集的表格行写入doc,然后清空缓冲区"""
if pending_table:
_add_table_to_doc(doc, pending_table)
pending_table.clear()
def _write_para(doc, line, font='宋体', size=12, bold=False,
first_indent=Cm(0.74), before=Pt(2), after=Pt(6),
alignment=None, is_heading=False):
"""写入正文段落(统一封装,方便多处复用)"""
p = doc.add_paragraph()
if alignment is not None:
p.alignment = alignment
p.paragraph_format.first_line_indent = first_indent
p.paragraph_format.line_spacing = Pt(22)
p.paragraph_format.space_before = before
p.paragraph_format.space_after = after
r = p.add_run(_clean_inline(line))
r.font.size = Pt(size)
r.font.bold = bold
cjk(r, font)
return p
# ============ Markdown → docx ============
def md_to_paragraphs(doc, content, add_page_break=True):
"""将markdown内容写入docx,正确处理表格和Mermaid图表"""
# --- Mermaid 预处理 ---
processed_content, rendered_images = process_mermaid_blocks(content)
# 建立 mermaid 代码 → 图片路径 的映射
mermaid_img_map = {}
if rendered_images:
for img_path in rendered_images:
img_name = os.path.basename(img_path)
# 从处理后的内容中提取 mermaid 代码作为 key(用标记)
for m in re.finditer(r'\[Mermaid图表已渲染,见附件:\s*(\S+)\]', processed_content):
fname = m.group(1)
if fname == img_name:
mermaid_img_map[m.group(0)] = img_path
break
lines = processed_content.split('\n')
i = 0
pending_table = []
mermaid_img_iter = iter(rendered_images) if rendered_images else iter([])
while i < len(lines):
line = lines[i].rstrip()
i += 1
# --- Mermaid 已渲染图片插入 ---
if '[Mermaid图表已渲染,见附件:' in line:
img_path = next(mermaid_img_iter, None)
if img_path and os.path.exists(img_path):
_flush_table(doc, pending_table)
try:
p = doc.add_paragraph()
run = p.add_run()
run.add_picture(img_path, width=Inches(5.5))
except Exception as e:
# 图片插入失败降级为文字提示
p = doc.add_paragraph()
r = p.add_run(line + ' [图片渲染失败]')
r.font.size = Pt(10); cjk(r, '宋体')
continue
if not line.strip():
_flush_table(doc, pending_table)
continue
if line.startswith('# '):
_flush_table(doc, pending_table)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(12)
p.paragraph_format.space_after = Pt(10)
r = p.add_run(_clean_inline(line[2:]))
r.font.size = Pt(18); r.font.bold = True; cjk(r, '黑体')
continue
if line.startswith('## '):
_flush_table(doc, pending_table)
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(10); p.paragraph_format.space_after = Pt(6)
r = p.add_run(_clean_inline(line[3:]))
r.font.size = Pt(14); r.font.bold = True; cjk(r, '楷体')
continue
if line.startswith('### '):
_flush_table(doc, pending_table)
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(8); p.paragraph_format.space_after = Pt(4)
r = p.add_run(_clean_inline(line[4:]))
r.font.size = Pt(12); r.font.bold = True; cjk(r, '仿宋')
continue
if line.startswith('#### '):
_flush_table(doc, pending_table)
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(6); p.paragraph_format.space_after = Pt(3)
r = p.add_run(_clean_inline(line[5:]))
r.font.size = Pt(11); r.font.bold = True; cjk(r, '仿宋')
continue
# markdown 表格行
if _is_table_line(line):
if not _is_separator_line(line):
pending_table.append(line)
continue
# 非表格行 → flush 缓存表格,写入正文段落
_flush_table(doc, pending_table)
p = doc.add_paragraph()
p.paragraph_format.first_line_indent = Cm(0.74)
p.paragraph_format.line_spacing = Pt(22)
p.paragraph_format.space_before = Pt(2)
p.paragraph_format.space_after = Pt(6)
r = p.add_run(_clean_inline(line))
r.font.size = Pt(12); cjk(r, '宋体')
# 处理末尾可能残留的表格
_flush_table(doc, pending_table)
if add_page_break:
doc.add_page_break()
# ============ 章节解析(错误隔离)============
def safe_parse_chapter(fpath: str) -> Optional[Tuple]:
fname = os.path.basename(fpath).replace('.txt', '')
seq = fname.split('-')[0]
try:
with open(fpath, 'r', encoding='utf-8') as fp:
content = fp.read()
except Exception as e:
print(f"[ERROR] 读取失败 {fname}: {e}")
return None
h2_entries = [l[3:].strip() for l in content.split('\n') if l.strip().startswith('## ')]
title = fname
for line in content.split('\n'):
line = line.strip()
if line.startswith('# '):
title = line[2:].strip()
break
return (seq, fname, title, content, h2_entries)
def parse_chapters(txt_files: List[str]) -> List[Tuple]:
seen_seq = set()
chapters, errors = [], []
for f in txt_files:
seq = os.path.basename(f).replace('.txt', '').split('-')[0]
if seq in seen_seq:
continue
result = safe_parse_chapter(f)
if result is None:
errors.append(os.path.basename(f)); continue
seen_seq.add(seq); chapters.append(result)
if errors:
print(f"[WARN] 以下章节解析失败(已跳过): {errors}")
return chapters
# ============ 字符统计 ============
def count_chars(text: str) -> int:
return len([c for c in text if c.strip()])
# ============ Glossary 生成(前置版)============
def generate_glossary(txt_files: List[str] = None, ref_text: str = "", max_terms=80) -> Dict[str, Any]:
"""从参考资料和章节内容中生成术语表"""
all_terms: Dict[str, int] = {}
# 从参考资料提取
if ref_text:
ref_terms = extract_terms_from_reference(ref_text, max_terms)
for item in ref_terms:
all_terms[item['term']] = all_terms.get(item['term'], 0) + item['count']
# 从章节内容提取
if txt_files:
stopwords = {'以及', '包括', '可以', '通过', '根据', '按照', '为了', '由于', '其中', '其他', '相关', '以上', '以下', '对于', '并且', '或者', '等等', '本项目', '本公司', '本系统'}
pattern = re.compile(r'[\u4e00-\u9fff]{4,}')
for f in txt_files:
try:
with open(f, 'r', encoding='utf-8') as fp:
content = fp.read()
for w in pattern.findall(content):
if w not in stopwords and len(w) >= 4:
all_terms[w] = all_terms.get(w, 0) + 1
except Exception:
continue
# 按频次排序取前max_terms
sorted_terms = sorted(all_terms.items(), key=lambda x: -x[1])[:max_terms]
glossary = {
"generated_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
"total_ref_chars": len(ref_text),
"terms": [{"term": t, "count": c} for t, c in sorted_terms]
}
with open(GLOSSARY_FILE, 'w', encoding='utf-8') as f:
json_module.dump(glossary, f, ensure_ascii=False, indent=2)
print(f"[GLOSSARY] 术语表已生成: {GLOSSARY_FILE}(共 {len(sorted_terms)} 个术语)")
return glossary
def load_glossary() -> Dict[str, Any]:
if os.path.exists(GLOSSARY_FILE):
try:
with open(GLOSSARY_FILE, 'r', encoding='utf-8') as f:
return json_module.load(f)
except Exception:
pass
return {"terms": []}
def glossary_to_prompt_text(glossary: Dict[str, Any], max_terms=30) -> str:
"""将术语表转为子Agent可读的提示文本"""
terms = glossary.get('terms', [])
if not terms:
return "(术语表暂无数据,完成 Batch A 后自动生成)"
display = terms[:max_terms]
lines = [f"- {t['term']}(出现{t['count']}次)" for t in display]
suffix = f"\n(共 {len(terms)} 个术语,仅展示前 {max_terms} 个)" if len(terms) > max_terms else ""
return '\n'.join(lines) + suffix
# ============ 大纲快照 ============
def save_outline_snapshot(plan: Dict[str, Any]):
"""保存规划师输出的大纲快照"""
lines = [f"# 文档大纲快照({datetime.now().strftime('%Y-%m-%d %H:%M')})"]
project_name = plan.get('project_name', '未知项目')
lines.append(f"\n项目:{project_name}\n")
for ch in plan.get('chapters', []):
lines.append(f"第{ch.get('seq','?')}章 | {ch.get('title','')} | Batch {ch.get('batch','')} | 约{ch.get('word_count',0)}字 | 依赖:{ch.get('dependencies',[])}")
content = '\n'.join(lines)
with open(OUTLINE_SNAPSHOT, 'w', encoding='utf-8') as f:
f.write(content)
print(f"[SNAPSHOT] 大纲快照已保存: {OUTLINE_SNAPSHOT}")
# ============ 批量版本快照 ============
def save_batch_snapshot(batch_label: str, batch_chapters: List[Tuple]):
"""保存每批完成后的章节内容快照"""
snapshot_file = f"{CHAPTERS_DIR}/snapshot_{batch_label}_{datetime.now().strftime('%Y%m%d_%H%M')}.md"
lines = [f"# {batch_label} 快照({datetime.now().strftime('%Y-%m-%d %H:%M')})"]
for seq, fname, title, content, _ in batch_chapters:
lines.append(f"\n---\n## 第{seq}章 {title}\n")
# 只保存前200字预览
preview = content[:300].replace('\n', ' ').strip()
lines.append(f"[预览] {preview}...")
with open(snapshot_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
print(f"[SNAPSHOT] 批次快照已保存: {snapshot_file}")
# ============ 跨章一致性审查 ============
def extract_quantities(text: str) -> Dict[str, str]:
qty = {}
pattern = re.compile(r'(\d+(?:\.\d+)?)\s*(万元|万元/年|万元\/年|人|人\/日|台|套|个|次|年|月|天|%)')
for m in pattern.finditer(text):
key = f"{m.group(1)}{m.group(2)}"
qty[key] = m.group(0)
return qty
def check_cross_chapter_consistency(chapters_data: List[Tuple]) -> List[Dict]:
issues = []
all_qty = [(seq, fname, extract_quantities(content)) for seq, fname, title, content, _ in chapters_data]
for i in range(len(all_qty) - 1):
seq_a, fname_a, qty_a = all_qty[i]
seq_b, fname_b, qty_b = all_qty[i + 1]
shared = set(qty_a.keys()) & set(qty_b.keys())
for key in shared:
ma = re.match(r'^(\d+(?:\.\d+)?)', key)
mb = re.match(r'^(\d+(?:\.\d+)?)', key)
if ma and mb:
try:
if float(ma.group(1)) != float(mb.group(1)):
issues.append({
"seq_a": seq_a, "seq_b": seq_b,
"item": key,
"value_a": qty_a[key], "value_b": qty_b[key]
})
except ValueError:
continue
return issues
# ============ 执行摘要 ============
def _build_summary(chapters_data, max_chars=800):
lines, total = [], 0
for seq, fname, title, content, h2_list in chapters_data:
para_lines = []
for line in content.split('\n'):
line = line.strip()
if not line or line.startswith('# ') or line.startswith('## ') or line.startswith('### '):
continue
para_lines.append(line)
if len(para_lines) >= 3:
break
if not para_lines:
continue
para_text = ''.join(para_lines[:2])
if total + len(para_text) > max_chars:
remaining = max_chars - total
if remaining > 50:
lines.append(para_text[:remaining] + '…')
break
lines.append(para_text)
total += len(para_text)
return lines or ['本报告对项目建设进行了全面可行性分析。']
# ============ 最终文档生成 ============
def generate_final_doc(chapters_data, page_estimates, output_path=FINAL_DOC, incremental=True):
"""
生成整合报告 docx。
incremental=True(默认):对比 hash,仅章节内容变化才重写该章;
无变化时跳过重写,直接复用已有章节 docx。
"""
plan = load_plan()
# --- 增量更新:检查哪些章节发生了变化 ---
changed_chapters = chapters_data
if incremental:
hashes = load_hashes()
changed_chapters = get_changed_chapters(chapters_data, hashes)
unchanged = [item for item in chapters_data if item not in changed_chapters]
changed_seqs = {item[0] for item in changed_chapters}
if unchanged and not changed_chapters:
print(f"[INCREMENTAL] 所有 {len(chapters_data)} 章内容未变化,跳过重写")
return None
elif unchanged:
print(f"[INCREMENTAL] {len(unchanged)} 章未变化,{len(changed_chapters)} 章需重写: {changed_seqs}")
doc = Document()
s = doc.sections[0]
s.page_height = Inches(11.69); s.page_width = Inches(8.27)
s.top_margin = Inches(1.0); s.bottom_margin = Inches(1.0)
s.left_margin = Inches(1.18); s.right_margin = Inches(1.18)
# 封面
for _ in range(6): doc.add_paragraph()
for txt, size, bold, font in [
(plan.get('org_name', '编制单位'), Pt(26), True, '黑体'),
(plan.get('project_name', '项目名称'), Pt(32), True, '黑体'),
]:
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run(txt); r.font.size = size; r.font.bold = bold; cjk(r, font)
for _ in range(3): doc.add_paragraph()
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run(plan.get('doc_type', '可行性研究报告'))
r.font.size = Pt(22); cjk(r, '楷体')
for _ in range(8): doc.add_paragraph()
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
unit = plan.get('编制单位', '编制单位')
build_time = plan.get('编制时间', datetime.now().strftime('%Y年%m月'))
r = p.add_run(f'编制单位:{unit}\n编制时间:{build_time}')
r.font.size = Pt(14); cjk(r, '宋体')
doc.add_page_break()
# 执行摘要
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(12); p.paragraph_format.space_after = Pt(10)
r = p.add_run('执行摘要'); r.font.size = Pt(18); r.font.bold = True; cjk(r, '黑体')
for pt in _build_summary(changed_chapters if changed_chapters else chapters_data):
p2 = doc.add_paragraph()
p2.paragraph_format.first_line_indent = Cm(0.74)
p2.paragraph_format.line_spacing = Pt(22)
p2.paragraph_format.space_after = Pt(6)
r2 = p2.add_run(pt); r2.font.size = Pt(12); cjk(r2, '宋体')
doc.add_page_break()
# 目录(使用真实 Word TOC 字段)
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(12); p.paragraph_format.space_after = Pt(10)
r = p.add_run('目 录'); r.font.size = Pt(18); r.font.bold = True; cjk(r, '黑体')
add_toc_entry(doc, '一', '执行摘要', 1, toc_type='summary')
seen = set()
for seq, fname, title, content, h2_list in (changed_chapters if changed_chapters else chapters_data):
if seq in seen: continue
seen.add(seq)
start = page_estimates.get(seq, (1, 0, 1))[0]
if not seq.isdigit(): continue
add_toc_entry(doc, f'第{int(seq)}章', title, start, toc_type='chapter')
doc.add_page_break()
# 各章节
target = changed_chapters if changed_chapters else chapters_data
for seq, fname, title, content, h2_list in target:
md_to_paragraphs(doc, content, add_page_break=True)
# --- 更新 hash 清单(增量记录)---
if incremental:
new_hashes = {}
for item in (changed_chapters if changed_chapters else chapters_data):
seq, content = item[0], item[3]
new_hashes[seq] = compute_content_hash(content)
# 合并未变化章节的旧 hash
old_hashes = load_hashes()
old_hashes.update(new_hashes)
save_hashes(old_hashes)
doc.save(output_path)
print(f"[DONE] 整合报告已保存: {output_path}")
return output_path
# ============ 整合报告主流程 ============
def generate_with_accurate_toc(txt_dir=CHAPTERS_DIR, final_doc=FINAL_DOC):
txt_files = sorted(glob.glob(f'{txt_dir}/*.txt'))
if not txt_files:
print(f"[ERROR] 未找到章节文件: {txt_dir}/*.txt"); return None
chapters_data = parse_chapters(txt_files)
if not chapters_data:
print("[ERROR] 所有章节解析均失败"); return None
print(f"[PARSE] 解析 {len(chapters_data)} 个章节")
# 更新术语表(含参考资料)
ref_text = load_reference()
generate_glossary(txt_files, ref_text=ref_text)
# 跨章一致性审查
issues = check_cross_chapter_consistency(chapters_data)
if issues:
print(f"[CONSISTENCY] 发现 {len(issues)} 个潜在不一致:")
for iss in issues:
print(f" - {iss['message']}")
else:
print("[CONSISTENCY] 跨章一致性检查通过 (OK)")
# 估算页码
pe = {}
cur = 7
for seq, fname, title, content, h2_list in chapters_data:
cc = count_chars(content)
ep = max(1, (cc + CHARS_PER_PAGE - 1) // CHARS_PER_PAGE)
pe[seq] = (cur, cc, ep); cur += ep
print("[BUILD] 生成整合报告...")
generate_final_doc(chapters_data, pe, output_path=final_doc)
# 纯文本版
md_path = final_doc.replace('.docx', '-纯文本.md')
with open(md_path, 'w', encoding='utf-8') as f:
f.write('\n\n---\n\n'.join(c for _, _, _, c, _ in chapters_data))
print(f"[MD] 纯文本版已保存: {md_path}")
return final_doc
# ============ 单章 docx 转换 ============
def convert_single_chapter_inline(txt_path, docx_path):
"""将txt章节文件转换为docx,正确解析markdown表格"""
try:
doc = Document()
s = doc.sections[0]
s.page_height = Inches(11.69); s.page_width = Inches(8.27)
s.top_margin = Inches(1.0); s.bottom_margin = Inches(1.0)
s.left_margin = Inches(1.18); s.right_margin = Inches(1.18)
with open(txt_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
pending_table = []
for line in lines:
line = line.rstrip()
if not line.strip():
_flush_table(doc, pending_table)
continue
if line.startswith('# '):
_flush_table(doc, pending_table)
p = doc.add_paragraph(); p.alignment = 1
p.paragraph_format.space_before = Pt(12); p.paragraph_format.space_after = Pt(10)
r = p.add_run(_clean_inline(line[2:])); r.font.size = Pt(18); r.font.bold = True; cjk(r, '黑体')
continue
if line.startswith('## '):
_flush_table(doc, pending_table)
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(10); p.paragraph_format.space_after = Pt(6)
r = p.add_run(_clean_inline(line[3:])); r.font.size = Pt(14); r.font.bold = True; cjk(r, '楷体')
continue
if line.startswith('### '):
_flush_table(doc, pending_table)
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(8); p.paragraph_format.space_after = Pt(4)
r = p.add_run(_clean_inline(line[4:])); r.font.size = Pt(12); r.font.bold = True; cjk(r, '仿宋')
continue
if line.startswith('#### '):
_flush_table(doc, pending_table)
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(6); p.paragraph_format.space_after = Pt(3)
r = p.add_run(_clean_inline(line[5:])); r.font.size = Pt(11); r.font.bold = True; cjk(r, '仿宋')
continue
# markdown 表格行
if _is_table_line(line):
if not _is_separator_line(line):
pending_table.append(line)
continue
# 普通正文段落
_flush_table(doc, pending_table)
p = doc.add_paragraph()
p.paragraph_format.first_line_indent = Cm(0.74)
p.paragraph_format.line_spacing = Pt(22)
p.paragraph_format.space_after = Pt(6)
r = p.add_run(_clean_inline(line)); r.font.size = Pt(12); cjk(r, '宋体')
# 处理末尾残留表格
_flush_table(doc, pending_table)
doc.save(docx_path)
return docx_path
except Exception as e:
print(f"[ERROR] 转换失败 {txt_path}: {e}")
raise
def _convert_worker(args) -> Tuple[str, bool, str]:
txt_path, docx_path = args
try:
convert_single_chapter_inline(txt_path, docx_path)
return (docx_path, True, '')
except Exception as e:
return (txt_path, False, str(e))
# ============ 批量并行转换 ============
def batch_convert_txt_to_docx(txt_dir=CHAPTERS_DIR, max_concurrent=8, progress_file=PROGRESS_FILE, incremental=True):
"""
批量将 txt 章节转换为 docx。
incremental=True(默认):对比内容hash,仅转换有变化的章节。
force=False:跳过已存在的docx(默认True)。
"""
txt_files = sorted(glob.glob(os.path.join(txt_dir, '*.txt')))
if not txt_files:
print(f"[ERROR] 未找到 .txt 文件"); return []
hashes = load_hashes() if incremental else {}
jobs = []
for tf in txt_files:
docx_path = tf.replace('.txt', '.docx')
content_hash = compute_content_hash(open(tf, 'r', encoding='utf-8').read())
if incremental and os.path.exists(docx_path):
if hashes.get(os.path.basename(tf)) == content_hash:
print(f" [SKIP] {os.path.basename(tf)} 内容未变化,跳过")
continue
jobs.append((tf, docx_path))
if not jobs:
print("[INFO] 所有章节已是最新(无变化),跳过转换")
return []
print(f"[BATCH] 待转换 {len(jobs)} 个章节,并发上限 {max_concurrent}")
completed, failed = [], []
with ProcessPoolExecutor(max_workers=max_concurrent) as executor:
futures = {executor.submit(_convert_worker, job): job for job in jobs}
for future in as_completed(futures):
docx_path, ok, err = future.result()
if ok:
# 更新 hash
txt_path = docx_path.replace('.docx', '.txt')
if os.path.exists(txt_path):
hashes[os.path.basename(txt_path)] = compute_content_hash(
open(txt_path, 'r', encoding='utf-8').read()
)
completed.append(docx_path); print(f" [OK] {os.path.basename(docx_path)}")
else:
failed.append((docx_path, err)); print(f" [FAIL] {os.path.basename(docx_path)}: {err}")
if incremental and completed:
save_hashes(hashes)
print(f"\n[BATCH] {len(completed)}/{len(jobs)} 成功,{len(failed)} 失败")
return completed
# ============ 进度文件 ============
def load_progress() -> Dict:
if os.path.exists(PROGRESS_FILE):
try:
with open(PROGRESS_FILE, 'r', encoding='utf-8') as f:
return json_module.load(f)
except Exception:
pass
return {"total": 0, "completed": 0, "batches": [], "current": ""}
# ============ CLI 入口 ============
if __name__ == '__main__':
if len(sys.argv) >= 2 and sys.argv[1] == '--convert-one':
if len(sys.argv) != 4:
print("用法: python integrate_report.py --convert-one <in.txt> <out.docx>"); sys.exit(1)
convert_single_chapter_inline(sys.argv[2], sys.argv[3])
print(f"saved: {sys.argv[3]}", flush=True); sys.exit(0)
elif len(sys.argv) >= 2 and sys.argv[1] == 'convert-batch':
txt_dir = sys.argv[2] if len(sys.argv) > 2 else CHAPTERS_DIR
batch_convert_txt_to_docx(txt_dir=txt_dir)
elif len(sys.argv) >= 2 and sys.argv[1] == 'glossary':
txt_files = sorted(glob.glob(f'{CHAPTERS_DIR}/*.txt'))
ref_text = load_reference()
generate_glossary(txt_files, ref_text=ref_text)
elif len(sys.argv) >= 2 and sys.argv[1] == 'check':
txt_files = sorted(glob.glob(f'{CHAPTERS_DIR}/*.txt'))
chapters_data = parse_chapters(txt_files)
issues = check_cross_chapter_consistency(chapters_data)
if not issues:
print("[OK] 跨章一致性检查通过,无不一致项")
else:
for iss in issues:
print(f"[WARN] {iss['message']}")
elif len(sys.argv) >= 2 and sys.argv[1] == 'status':
prog = load_progress()
print(f"进度: {prog.get('completed',0)}/{prog.get('total','?')}")
if prog.get('current'): print(f"状态: {prog['current']}")
elif len(sys.argv) >= 2 and sys.argv[1] == 'ref':
# 仅查看/更新参考资料
if len(sys.argv) >= 3:
action = sys.argv[2]
if action == 'show':
ref = load_reference()
print(f"参考资料: {len(ref)} 字符")
print(ref[:500] if ref else "(空)")
elif action == 'clear':
save_reference("")
print("参考资料已清空")
else:
ref = load_reference()
print(f"当前参考资料: {len(ref)} 字符")
else:
# 默认:生成整合报告
txt_dir = sys.argv[1] if len(sys.argv) > 1 else CHAPTERS_DIR
result = generate_with_accurate_toc(txt_dir=txt_dir)
if result:
print(f"\n[DONE] 整合报告生成完成: {result}")
FILE:parallel_tracker.py
"""
parallel_tracker.py
===================
多子Agent并行撰写可视化追踪模块
工作原理:
1. 主Agent使用 sessions_spawn 并行启动多个子Agent
2. 每个子Agent启动后向 TRACKER_FILE 写入自己的状态
3. 主Agent周期性地读取 TRACKER_FILE 并渲染可视化表格
使用方式:
from parallel_tracker import Tracker, update_chapter_status
# 子Agent端:启动时注册
tracker = Tracker()
tracker.register(seq="04", title="系统架构设计", batch="B")
tracker.update(seq="04", phase="writing", progress=50, note="撰写功能模块...")
# 子Agent端:完成后标记
tracker.update(seq="04", phase="done", progress=100)
"""
import json, os, time, sys, threading
from datetime import datetime
from typing import Dict, List, Optional, Any
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
# ============ 配置 ============
CHAPTERS_DIR = 'F:/agent/chapters'
TRACKER_FILE = f'{CHAPTERS_DIR}/writing_tracker.json'
# ============ 追踪器 ============
_GLOBAL_TRACKER: Optional['Tracker'] = None
_GLOBAL_LOCK = threading.Lock()
class Tracker:
"""多子Agent并行撰写状态追踪器(线程安全单例)"""
def __init__(self, tracker_file: str = TRACKER_FILE):
self.tracker_file = tracker_file
self._ensure_file()
@staticmethod
def get_instance(tracker_file: str = TRACKER_FILE) -> 'Tracker':
"""获取单例实例(线程安全)"""
global _GLOBAL_TRACKER
if _GLOBAL_TRACKER is None:
with _GLOBAL_LOCK:
if _GLOBAL_TRACKER is None:
_GLOBAL_TRACKER = Tracker(tracker_file)
return _GLOBAL_TRACKER
def _ensure_file(self):
if not os.path.exists(self.tracker_file):
self._write({})
def _read(self) -> Dict[str, Any]:
with _GLOBAL_LOCK:
try:
with open(self.tracker_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
return {}
def _write(self, data: Dict[str, Any]):
with _GLOBAL_LOCK:
with open(self.tracker_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def register(self, seq: str, title: str, batch: str = "", agent_id: str = ""):
"""子Agent启动时注册"""
data = self._read()
data[seq] = {
"seq": seq,
"title": title,
"batch": batch,
"agent_id": agent_id,
"phase": "registered", # registered | outline | writing | reviewing | done | error
"progress": 0,
"note": "已注册,等待启动...",
"started_at": datetime.now().strftime('%H:%M:%S'),
"updated_at": datetime.now().strftime('%H:%M:%S'),
}
self._write(data)
return self
def update(self, seq: str, phase: str, progress: int = None,
note: str = "", error: str = ""):
"""
更新子Agent撰写状态
phase: registered | outline | writing | reviewing | done | error
progress: 0-100
"""
data = self._read()
if seq not in data:
# 未注册,自动注册
data[seq] = {"seq": seq, "title": seq, "batch": ""}
entry = data[seq]
entry["phase"] = phase
if progress is not None:
entry["progress"] = progress
if note:
entry["note"] = note
if error:
entry["error"] = error
entry["updated_at"] = datetime.now().strftime('%H:%M:%S')
self._write(data)
return self
def mark_done(self, seq: str, note: str = "已完成"):
return self.update(seq, phase="done", progress=100, note=note)
def mark_error(self, seq: str, error: str):
return self.update(seq, phase="error", note="出错", error=error)
def get_status(self) -> Dict[str, Any]:
return self._read()
def clear(self):
"""清空追踪状态(每批次开始前调用)"""
self._write({})
def get_summary(self) -> Dict[str, int]:
data = self._read()
phases = {}
for entry in data.values():
p = entry.get("phase", "unknown")
phases[p] = phases.get(p, 0) + 1
return phases
# ============ 可视化渲染 ============
TRACKER_FILE_FOR_PRINT = TRACKER_FILE # 模块级引用
def _progress_bar(progress: int, width: int = 12) -> str:
"""渲染进度条:▓░░░░░░░░░░"""
filled = int(width * progress / 100)
return '▓' * filled + '░' * (width - filled)
def _phase_emoji(phase: str) -> str:
emoji_map = {
"registered": "⏳",
"outline": "📋",
"writing": "✍️",
"reviewing": "🔍",
"done": "✅",
"error": "❌",
}
return emoji_map.get(phase, "⚪")
def render_progress_table(tracker_file: str = TRACKER_FILE) -> str:
"""
渲染当前并行撰写状态表格
返回格式:
╔══════════════════════════════════════════════════════════════╗
║ 📊 多子Agent并行撰写进度监控 ║
╠══════════════════════════════════════════════════════════════╣
║ 04 系统架构设计 ✍️ writing ▓▓▓▓▓▓░░░░ 50% 撰写功能模块... ║
║ 05 技术路线 ✍️ writing ▓▓▓░░░░░░░ 25% 撰写技术选型... ║
║ 06 功能模块设计 ⏳ registered ───────── 0% 等待启动... ║
╚══════════════════════════════════════════════════════════════╝
"""
try:
with open(tracker_file, 'r', encoding='utf-8') as f:
data = json.load(f)
except Exception:
return "(追踪文件暂不可用)"
if not data:
return "(暂无并行撰写任务)"
# 按 seq 排序
sorted_entries = sorted(data.values(), key=lambda x: x.get('seq', '0'))
# 计算全局进度
total = len(sorted_entries)
done = sum(1 for e in sorted_entries if e.get('phase') == 'done')
errors = sum(1 for e in sorted_entries if e.get('phase') == 'error')
overall_pct = int((done / total * 100)) if total > 0 else 0
header = (
f"╔══════════════════════════════════════════════════════════════╗\n"
f"║ 📊 多子Agent并行撰写进度监控 [{done}/{total} 完成"
f"{' ❌' + str(errors) if errors > 0 else ''}] 总体 {overall_pct}% ║\n"
f"╠══════════════════════════════════════════════════════════════╣"
)
footer = "╚══════════════════════════════════════════════════════════════╝"
rows = []
for entry in sorted_entries:
seq = entry.get('seq', '??').rjust(2)
title = entry.get('title', '')[:14].ljust(14)
phase_icon = _phase_emoji(entry.get('phase', ''))
phase_name = entry.get('phase', '').rjust(10)
progress = entry.get('progress', 0)
bar = _progress_bar(progress)
pct = str(progress).rjust(3) + '%'
note = (entry.get('note', '') or '').strip()[:20].ljust(20)
batch = entry.get('batch', '')
batch_str = f"[{batch}] " if batch else " "
row = f"║ {seq} {batch_str}{title} {phase_icon} {phase_name} {bar} {pct} {note} ║"
rows.append(row)
return '\n'.join([header] + rows + [footer])
def print_progress(tracker_file: str = TRACKER_FILE):
"""打印进度表格到标准输出(供 exec 调用)"""
print(render_progress_table(tracker_file), flush=True)
# ============ 轮询监控器 ============
class ProgressMonitor:
"""
定期轮询 tracker 文件并打印进度的监控器
用于在子Agent并行撰写时,主session展示实时进度
"""
def __init__(self, tracker_file: str = TRACKER_FILE, interval_sec: float = 8.0):
self.tracker_file = tracker_file
self.interval_sec = interval_sec
self._running = False
def start(self, duration_sec: float = None):
"""
启动监控循环
duration_sec: 监控持续秒数,None表示直到所有任务完成
"""
self._running = True
import time
start = time.time()
last_seen_done = set()
print(f"[MONITOR] 启动进度监控(间隔{self.interval_sec}秒)", flush=True)
while self._running:
try:
with open(self.tracker_file, 'r', encoding='utf-8') as f:
data = json.load(f)
entries = list(data.values())
if not entries:
time.sleep(self.interval_sec)
continue
# 检查是否全部完成
done_seqs = {e['seq'] for e in entries if e.get('phase') == 'done'}
error_seqs = {e['seq'] for e in entries if e.get('phase') == 'error'}
# 打印进度
os.system('cls' if os.name == 'nt' else 'clear')
print(render_progress_table(self.tracker_file), flush=True)
# 新完成任务时提示
new_done = done_seqs - last_seen_done
if new_done:
print(f"\n✅ 新完成:第 {[e['seq'] for e in entries if e['seq'] in new_done]} 章", flush=True)
last_seen_done = done_seqs
# 检查是否全部结束
all_done = len(done_seqs) + len(error_seqs) == len(entries)
if all_done:
print(f"\n[MONITOR] 所有章节撰写完成!", flush=True)
break
# 检查超时
if duration_sec and (time.time() - start) >= duration_sec:
print(f"\n[MONITOR] 监控超时({duration_sec}秒)", flush=True)
break
time.sleep(self.interval_sec)
except Exception as e:
print(f"[MONITOR] 轮询异常: {e}", flush=True)
time.sleep(self.interval_sec)
def stop(self):
self._running = False
# ============ 子Agent端辅助函数 ============
def get_tracker() -> Tracker:
"""获取Tracker单例(子Agent端推荐使用)"""
return Tracker.get_instance()
def chapter_register(seq: str, title: str, batch: str = ""):
"""子Agent启动时调用:注册章节撰写任务"""
Tracker().register(seq=seq, title=title, batch=batch)
def chapter_update(seq: str, phase: str, progress: int = None, note: str = ""):
"""子Agent撰写过程中调用:更新进度"""
Tracker().update(seq=seq, phase=phase, progress=progress, note=note)
def chapter_done(seq: str, note: str = "已完成"):
"""子Agent完成时调用:标记完成"""
Tracker().mark_done(seq=seq, note=note)
def chapter_error(seq: str, error: str):
"""子Agent出错时调用:标记错误"""
Tracker().mark_error(seq=seq, error=error)
# ============ CLI 入口 ============
if __name__ == '__main__':
if len(sys.argv) >= 2:
cmd = sys.argv[1]
tracker = Tracker()
if cmd == 'show' or len(sys.argv) == 2:
print(render_progress_table())
elif cmd == 'clear':
tracker.clear()
print("追踪状态已清空")
elif cmd == 'status':
summary = tracker.get_summary()
print(f"当前状态: {summary}")
total = sum(summary.values())
done = summary.get('done', 0)
print(f"进度: {done}/{total} 完成")
elif cmd == 'wait':
# 阻塞监控模式
import time
print("开始监控... Ctrl+C 停止")
try:
while True:
os.system('cls' if os.name == 'nt' else 'clear')
print(render_progress_table())
time.sleep(8)
except KeyboardInterrupt:
print("\n监控已停止")
elif cmd == 'register' and len(sys.argv) >= 4:
_, _, seq, title, *rest = sys.argv
batch = rest[0] if rest else ""
tracker.register(seq, title, batch)
print(f"已注册:第{seq}章 {title} [{batch}]")
elif cmd == 'update' and len(sys.argv) >= 4:
_, _, seq, phase, *rest = sys.argv
progress = int(restr[0]) if rest and rest[0].isdigit() else None
note = rest[1] if len(restr := rest) > 1 else ""
tracker.update(seq, phase, progress, note)
print(f"已更新:第{seq}章 {phase} {progress or ''}% {note}")
elif cmd == 'done' and len(sys.argv) >= 3:
seq = sys.argv[2]
tracker.mark_done(seq)
print(f"已标记完成:第{seq}章")
else:
print(render_progress_table())
FILE:references/bug_fix_guide.md
# Bug Troubleshooting & Forced Rebuild
## Fixed: Table Rendering Distortion
### Problem Description
In the generated docx, tables have only 1 column with content split character-by-character; or column count far exceeds expectation (e.g., 4-column table becomes 60+ columns).
### Root Cause
Bug in `_flush_table` function in `integrate_report.py`:
```python
# ❌ Buggy code (early v3)
def _flush_table(doc, pending_table):
if pending_table:
_add_table_to_doc(doc, pending_table) # ← Passing raw string list!
pending_table.clear()
```
- `pending_table` stores `['| Col1 | Col2 | Col3 |', ...]` (string list)
- `_add_table_to_doc` uses `max(len(r) for r in rows)` to calculate column count
- Calling `len()` on a string gives character count (17), not cell count (3)
- Result: 4-column table → 63 columns → each character occupies one cell → completely distorted
### Fix
```python
# ✅ Correct code (current version)
def _flush_table(doc, pending_table):
if pending_table:
parsed_rows = _parse_md_table(pending_table) # ← NEW: parse to 2D array first
_add_table_to_doc(doc, parsed_rows) # ← Pass parsed array
pending_table.clear()
```
### Validation
```python
from docx import Document
doc = Document('F:/agent/整合报告.docx')
for t in doc.tables:
print(f'{len(t.rows)} rows x {len(t.columns)} cols')
# Normal column count: 2~8 columns
# If you see 15+, 30+, 60+ columns → Bug still exists
```
---
## Fixed: Cover Style Comparison Type Error
### Problem Description
`cover_style` in `plan.json` is an integer (e.g., `4`), but code compares as string, causing cover to always take the generic branch — styled cover doesn't apply.
### Root Cause
```python
# ❌ Buggy code
cover_style = plan.get('cover_style', '4')
if cover_style == '4': # Integer 4 != String '4', always False
```
### Fix
```python
# ✅ Correct code
cover_style = str(plan.get('cover_style', '4'))
if cover_style == '4': # String comparison, works correctly
```
---
## Fixed: RGB Color Assignment Error
### Problem Description
Using `eval(f'0x{hex_color}')` to assign color causes `run.font.color.rgb` to receive an integer instead of an `RGBColor` object, throwing an error.
### Root Cause
```python
# ❌ Buggy code
run.font.color.rgb = eval(f'0x{H1_TEXT}') # eval('0xFFFFFF') → 16777215 (int)
# ValueError: rgb color value must be RGBColor object, got <class 'int'>
```
### Fix
```python
# ✅ Correct code
from docx.shared import RGBColor # ← Must import
run.font.color.rgb = RGBColor.from_string(H1_TEXT)
```
---
## Fixed: Incremental Cache Causing New Code to Not Take Effect
### Problem Description
After modifying core logic in `integrate_report.py` and regenerating, incremental mode skips all chapters.
### Root Cause
Python caches compiled `.pyc` files. After modifying `.py`, if cache isn't deleted, the imported code is still the old version. Also `content_hashes.json` causes rewrite skipping.
### Fix
After every code change, do both:
```bash
# 1. Delete .pyc cache
del "C:\Users\Administrator\AppData\Roaming\LobsterAI\SKILLs\long-doc-agent\__pycache__\integrate_report.cpython-311.pyc"
# 2. Delete incremental hash
del F:\agent\chapters\content_hashes.json
# 3. Regenerate
python integrate_report.py
```
---
## Forced Rebuild
### Steps
```bash
# 1. Delete incremental cache and old report
del F:\agent\chapters\content_hashes.json
del F:\agent\整合报告.docx
# 2. Delete .pyc cache (required after code changes)
del "C:\Users\Administrator\AppData\Roaming\LobsterAI\SKILLs\long-doc-agent\__pycache__\integrate_report.cpython-311.pyc"
# 3. Regenerate
cd "C:\Users\Administrator\AppData\Roaming\LobsterAI\SKILLs\long-doc-agent"
python integrate_report.py
```
---
## New: RGBColor Property Access (python-docx 1.2.0)
### Problem Description
Error when running `make_docx.py`: `AttributeError: 'RGBColor' object has no attribute 'red'`
### Root Cause
python-docx 1.2.0's `RGBColor` object doesn't support `.red / .green / .blue` property access; must use index access.
### Fix
```python
# ❌ Wrong
'{:02X}{:02X}{:02X}'.format(rgb.red, rgb.green, rgb.blue)
# ✅ Correct
'{:02X}{:02X}{:02X}'.format(rgb[0], rgb[1], rgb[2])
```
---
## New: Cover Function Must Not Modify Global Page Margins
### Problem Description
Code in `add_cover()` setting `section.left_margin=0` etc. propagates to all pages after the cover, causing body text to fill the entire page (no margins).
### Root Cause
Word's Section properties persist across pages; margins set on the cover affect the entire document.
### Fix
Use a full-page table for the background color in the cover, **do not** modify any section margin properties. Set body margins once in `main()`.
```python
# ❌ Wrong
def add_cover(doc):
sec = doc.sections[0]
sec.left_margin=Inches(0) # ← Affects all pages!
...
# ✅ Correct: only add table, don't touch section
def add_cover(doc):
tbl_ = doc.add_table(rows=1, cols=1)
cell = tbl_.rows[0].cells[0]
# Just fill the table across the full page, don't touch section margin
```
---
## New: Auto-Rename When File Is Held Open
### Problem Description
If the generated docx file is already open in WPS/Word, saving again raises `PermissionError`.
### Fix
Add `_v2` suffix to filename (auto-increment), avoiding conflict with open files. In code:
```python
out_name = 'Hospital_Personnel_Location_Management_System_Proposal.docx'
out_path = os.path.join(out_dir, out_name)
if os.path.exists(out_path):
# File exists, add v2/v3... to avoid conflict
base, ext = os.path.splitext(out_name)
counter = 2
while os.path.exists(os.path.join(out_dir, f'{base}_v{counter}{ext}')):
counter += 1
out_name = f'{base}_v{counter}{ext}'
out_path = os.path.join(out_dir, out_name)
```
---
## New: write Tool Has 50KB Line Limit — Large Scripts Must Use Chunked Writing
### Problem Description
Using `write` tool to write Python scripts > ~50KB or ~2000 lines results in truncated content (only partial code written).
### Root Cause
The write tool has a per-file size limit.
### Fix
Write large scripts in two steps:
```python
# Step 1: Write main file (excluding trailing main() call)
with open('make_docx.py', 'w', encoding='utf-8') as f:
f.write(main_content) # Main content
# Step 2: Append trailing portion
closing = """
def main():
... # Trailing content
if __name__ == '__main__':
main()
"""
with open('make_docx.py', 'a', encoding='utf-8') as f:
f.write(closing)
```
---
## Other Common Issues
### Symptom: Table content all shows as `|`
`_parse_md_table` was not called. Confirm `_flush_table` contains `parsed_rows = _parse_md_table(pending_table)`.
### Symptom: Incremental mode skips modified chapters
Delete `content_hashes.json` to force full rebuild.
### Symptom: Sub-Agent-written txt contains garbled text
Sub-Agent output used wrong encoding. Ensure sub-agents save with `encoding='utf-8'`.
FILE:references/phase0_guide.md
# Phase 0: Requirements Confirmation Full Flow
## Trigger Conditions
User's first writing request ("I want to write xxx"/"help me write a feasibility report"/"write a xxx report")
## Four-Step Confirmation Flow
### Step 1 — Confirm Writing Topic
```
Please tell me the core information for this document:
1. What is the document topic?
(e.g., XX City People's Hospital Medical Asset Refined Management Solution)
2. What type of document is it?
(e.g., Feasibility Study Report / Technical Proposal / Business Plan)
3. Who is the primary audience?
(e.g., Hospital Management / Superior Regulatory Authority / Investors)
4. Overall style?
(e.g., Professional & Rigorous / Concise & Clear)
5. Any special requirements or constraints?
(e.g., Must include budget section / No more than 10 chapters, etc.)
```
### Step 2 — Confirm Writing Background
```
Please provide or describe the background information for this project/topic:
1. What is the project background?
(e.g., Current hospital asset management status, problems faced)
2. What are the construction goals?
(e.g., Improve asset utilization rate, control costs)
3. Any specific industry background?
(e.g., National policies, industry trends)
```
### Step 3 — Provide Reference Materials (Most Important)
```
Please provide reference materials related to this writing task (provide at least one):
A. Upload file: Send local file path or paste content directly
B. Feishu document: Provide document name or link (I will search via RAG)
C. Paste directly: Send reference text directly to this assistant
D. Not provided for now (skip, write using general background knowledge)
⚠️ Strongly recommend providing reference materials!
The more reference material, the more business-aligned the content, the higher the output quality.
Reference materials will be injected as the primary RAG knowledge source into each chapter's writing context.
```
### Step 4 — Outline Confirmation
After planner outputs the outline, display it to the user for confirmation:
```
📋 Planning outline generated. Please confirm the following chapter structure:
Project: XX City People's Hospital Medical Asset Full Lifecycle Refined Management Solution
Type: Feasibility Study Report | Audience: Hospital Management
Chapter Outline:
1. Chapter 01 Project Overview (Batch A, ~2500 words)
2. Chapter 02 Construction Background & Necessity (Batch A, ~3000 words)
...
Please confirm:
A. Outline OK, start writing
B. Need to adjust outline (please specify which chapters need modification/addition/deletion)
C. Cancel this writing task
```
## Saving Reference Materials
After user confirmation, save reference materials to `F:/agent/chapters/reference_material.txt`:
```python
with open('F:/agent/chapters/reference_material.txt', 'w', encoding='utf-8') as f:
f.write(reference_text)
```
FILE:references/phase1_guide.md
# Phase 1: Planner Full Prompt Template
## Execution Steps
1. Load `F:/agent/chapters/reference_material.txt` summary (first 3000 characters) as `reference_summary`
2. Replace `{xxx}` placeholders in the template below with actual values
3. Write output to `F:/agent/chapters/plan.json`
## Prompt Template
```
You are a professional project planner. The user needs to write a 【{doc_type}】 on the topic of "{topic}".
## User-Provided Background Information
{background}
## Reference Material Summary (Priority Reference)
{reference_summary}
Please complete the following tasks:
1. Create a detailed document outline (down to H3 headings)
2. Annotate core writing points for each chapter
3. Identify RAG search keywords for each chapter (≤3 per chapter)
4. Evaluate complexity of each chapter, mark key chapters
5. Identify chapter dependencies (which chapters must be completed before others can be written)
**Chapter Dependency Rules**:
- Type 1 (no dependencies, write first): Overview, Background, Current Analysis, Technology Selection
- Type 2 (depends on Type 1): Overall Design, Detailed Function Design
- Type 3 (depends on several preceding chapters): Implementation Plan, Testing Plan, Deployment Plan
- Type 4 (can write independently or last): Training Plan, Acceptance Plan, Appendices, Conclusion
**Reference Materials Prohibition**
Actively exclude content unrelated to the topic during planning (e.g., infusion monitoring systems, etc.).
Write the following structured information to F:/agent/chapters/plan.json:
{
"project_name": "Project Name",
"doc_type": "Document Type",
"chapters": [
{
"seq": "01",
"title": "Chapter Title",
"brief": "Writing Points",
"feishu_keywords": ["k1", "k2"],
"web_keywords": ["k1", "k2"],
"word_count": 3000,
"batch": "A",
"dependencies": [],
"status": "pending"
}
]
}
```
## plan.json Field Descriptions
| Field | Description |
|-------|-------------|
| `seq` | Chapter sequence number, 2-digit string ("01", "02") |
| `title` | Chapter title |
| `brief` | Core writing points (50-100 characters) |
| `feishu_keywords` | Feishu knowledge base search keywords, max 3 |
| `web_keywords` | Web search keywords, max 3 |
| `word_count` | Target word count (body text, excluding headings) |
| `batch` | Batch label ("A"/"B"/"C", same batch can be written in parallel) |
| `dependencies` | Dependent chapter seq list, e.g. `["01", "02"]` |
| `status` | Status: `pending`/`writing`/`txt_done`/`confirmed` |
## Post-Execution Actions
```bash
# 1. Generate initial glossary (extracted from reference materials)
python integrate_report.py glossary
# 2. Save outline snapshot
python integrate_report.py save-outline
# 3. Display outline to user for confirmation
```
FILE:references/phase2_guide.md
# Phase 2: Sub-Agent Full Prompt Template
## Template Variable Descriptions
| Variable | Source |
|----------|--------|
| `{seq}` | seq field of this chapter in plan.json |
| `{title}` | title field of this chapter in plan.json |
| `{batch}` | batch field of this chapter in plan.json |
| `{topic}` | Document topic |
| `{audience}` | Target audience |
| `{style}` | Overall style |
| `{word_count}` | word_count field of this chapter in plan.json |
| `{reference_summary}` | Reference material summary (first 3000 characters) |
| `{glossary_summary}` | Glossary summary (first 30 entries) |
| `{dependency_chapters}` | Title list of dependent chapters |
| `{chapter_brief}` | brief field of this chapter in plan.json |
| `{feishu_keywords}` | feishu_keywords from plan.json |
| `{web_keywords}` | web_keywords from plan.json |
| `{index}` | 2-digit sequential number (01, 02...) |
| `{short_name}` | Chapter short name (used in filenames) |
## Sub-Agent Prompt (Full Version)
```python
import sys
sys.path.insert(0, r'C:\Users\Administrator\AppData\Roaming\LobsterAI\SKILLs\long-doc-agent')
from parallel_tracker import chapter_register, chapter_update, chapter_done
chapter_register(seq='{seq}', title='{title}', batch='{batch}')
You are a professional document writing expert, responsible for writing the 【{chapter_title}】 chapter of the feasibility report.
## Basic Information
- Document Topic: {topic}
- Target Audience: {audience}
- Overall Style: {style}
- This Chapter's Word Target: {word_count} words
## Reference Materials (Priority Use)
{reference_summary}
## Glossary Reference (Must Use Consistent Terminology)
{glossary_summary}
## Dependencies
This chapter depends on the following completed chapters:
{dependency_chapters}
## This Chapter's Writing Points
{chapter_brief}
## RAG Search (Supplementary Reference)
- Feishu Knowledge Base: keywords {feishu_keywords}
- Web Search (backup): keywords {web_keywords}
## Writing Requirements
1. Content must be professional and rigorous, conforming to feasibility report standards
2. Prioritize citing facts and data from reference materials
3. Terminology usage must be consistent with the glossary
4. Word count: approximately {word_count} words
5. Output format: Markdown
## ⚠️ Markdown Table Format (Must Follow)
When inserting tables, you MUST strictly follow this format, otherwise tables will be distorted in docx conversion:
Correct format:
| Col1 | Col2 | Col3 |
|---|---|---|
| Content1 | Content2 | Content3 |
Key points:
- Separator row format must be `|---|---|---|` (leading/trailing `|` required)
- All rows must have the same column count as the header — mismatch causes column displacement
- Cell content should avoid containing `|` (use `~` or `-` for ranges)
## Progress Update
After completing each ## H2 section heading, call:
chapter_update(seq='{seq}', phase='writing', progress=30, note='Writing in progress...')
## Output: Generate Plain Text .txt Only
After completing the writing:
1. Save to F:/agent/chapters/{index:02d}-{short_name}.txt
2. Call chapter_done(seq='{seq}', note='Completed')
3. Update this chapter's status to 'txt_done' in plan.json
```
## Per-Batch Execution Flow (Main Agent Side, Fully Automatic)
```python
# 1. Display outline/current status (display only, no user confirmation)
print(f"Current batch: Batch {label}")
print(f"Chapters to write: {chapters_list}")
print(f"Estimated parallelism: {n} chapters")
# 2. Clear previous batch tracking state
from parallel_tracker import Tracker
Tracker().clear()
# 3. Parallel launch sub-agents (≤5 per batch, automatically execute all batches)
for subagent_task in batch_tasks:
sessions_spawn(
task=subagent_task,
runtime="subagent",
runTimeoutSeconds=300,
mode="run"
)
# 4. Monitor progress in background (automatically wait for this batch to complete)
# python parallel_tracker.py wait
# 5. After this batch completes, automatically proceed to next batch (no user confirmation)
# If this is the last batch, automatically execute:
from integrate_report import batch_convert_txt_to_docx
batch_convert_txt_to_docx(txt_dir='F:/agent/chapters', max_concurrent=8)
```
## Batch Completion Notification
WeChat notification is automatically sent after each batch completes, no manual intervention needed. If any chapter needs modification, you can notify the main agent at any time (supports small changes via direct .txt editing, or large changes via full chapter regeneration).
FILE:references/table_format_guide.md
# Markdown Table Format Specification
Sub-agents must strictly follow this specification when inserting tables in `.txt` files. Incorrect format causes table distortion during docx conversion.
## Correct Format
```
| Col1 | Col2 | Col3 |
|---|---|---|
| Content1 | Content2 | Content3 |
| Content4 | Content5 | Content6 |
```
## Six Key Rules
1. **Separator row must include leading and trailing `|`**
- ✅ Correct: `|---|---|---|`
- ❌ Wrong: `---|---|---` (missing leading/trailing `|`)
- ❌ Wrong: `|---|:---|:---|` (missing leading/trailing `|` on separator row)
2. **All rows (including data rows) must have leading and trailing `|`**
- Each row format: `| Cell1 | Cell2 | Cell3 |`
3. **All rows must have the same column count as the header**
- If header has 4 columns, all data rows must also have 4 columns
- Mismatch causes column displacement in docx
4. **Separator row only allows `-`, `:`, `|` and spaces**
- ✅ `|---|`, `| :--- |` (alignment markers)
- ❌ `|===|` (`=` not allowed)
- ❌ `|--|--|` (missing leading/trailing `|`)
5. **Cell content must not contain line breaks**
- Cell content must be completed on a single line
6. **Cell content should avoid containing `|` character**
- Use `~` or `-` for ranges: `25—45` not `25|45`
- If `|` must be included, escape it (not recommended)
## Common Error Examples
| Error Type | Wrong | Correct |
|------------|-------|---------|
| Missing leading/trailing `\|` | `\|---\|---\|---` | `\|---\|---\|---\|` |
| Inconsistent row/column count | `\|A\|B\|C\|` followed by `\|1\|2\|` (missing column) | `\|A\|B\|C\|` followed by `\|1\|2\| \|` |
| Separator row uses `=` | `\|====\|====\|` | `\|---\|---\|` |
| Cell contains `\|` | `\|25\|45\|` (range) | `\|25~45\|` |
## Recommended Symbols for Cell Content
| Purpose | Recommended Symbol | Example |
|---------|-------------------|---------|
| Numeric range | `~` or `—` | `25~45`, `25—45` |
| Percentage | `%` | `30%` |
| Rating | `★` (avoid `\|`) | `★★★☆☆` |
| Notes/Remarks | Write directly | `Including equipment maintenance service` |
## Validation Method
After generating docx, use this command to check if column counts are reasonable (normally 2~8 columns):
```python
from docx import Document
doc = Document('F:/agent/整合报告.docx')
for t in doc.tables:
print(f'{len(t.rows)} rows x {len(t.columns)} cols')
# Column count > 15 usually indicates table format error
```
## Why Format Errors Cause Distortion
`_add_table_to_doc` internally uses `max(len(r) for r in rows)` to calculate column count:
- After correct parsing: `rows[0] = ['Col1', 'Col2', 'Col3']`, `len = 3`
- When raw string is passed: `rows[0] = '| Col1 | Col2 | Col3 |'`, `len = 17` (character count)
17 columns vs 3 columns → each character occupies one cell → table completely distorted.
ITIL 5 Manager - Elite IT Service Management Advisor specializing in ITSM, FinOps, and IT governance using ITIL 5 DPSM framework.
---
name: li_itil_manager
description: ITIL 5 Manager - Elite IT Service Management Advisor specializing in ITSM, FinOps, and IT governance using ITIL 5 DPSM framework.
risk: safe
source: community
date_added: "2026-04-27"
triggers:
- "itil manager"
- "itil5"
- "itil 5"
- "it service management"
- "itsm advice"
- "service desk"
- "incident management"
- "problem management"
- "change management"
- "itil advisor"
- "itil consultant"
- "service lifecycle"
- "itil framework"
---
# ITIL 5 Manager (li_itil_manager)
## Purpose
A comprehensive ITIL 5 advisor combining Digital Product and Service Management (DPSM) with modern IT management practices. Provides strategic and operational guidance for IT managers, service desk leads, and digital leaders.
## When to Use
- Need ITIL 5 implementation guidance
- Managing IT service delivery and support
- Building or improving ITSM processes
- Implementing FinOps in IT operations
- Bridging IT and business communication
## Core Capabilities
- **ITIL 5 DPSM:** Digital Product and Service Management approach
- **Service Value Chain:** Plan, Engage, Design & Transform, Obtain/Build, Deliver & Support, Improve
- **Process Optimization:** Incident, Problem, Change, Knowledge, and Service Request Management
- **Executive Communication:** C-level storytelling and ROI reporting
- **FinOps Integration:** Connecting service cost to business value
## ITIL 5 Guiding Principles
1. Focus on value
2. Progress iteratively
3. Collaborate and promote visibility
4. Think and work holistically
5. Keep it simple
6. Optimize and automate
7. Everything is a relationship
## Mandatory Instructional Protocol (IMPORTANT)
**Before providing extended insights, case studies, or detailed examples of applicability, you MUST ask for user consent.**
* **Protocol:** Provide the core answer/solution first. Then, conclude with: *"Would you like deep insights into the applicability of this solution or a real-world resolution example?"*
* **Action:** Only provide the extra depth if the user explicitly confirms.
## Expert Instructions
### 1. Service Strategy & Value Co-creation
- Treat all IT services as Digital Products
- Define Service Offerings that support customer outcomes
- Establish Service Relationships with stakeholders
- Map service value to business outcomes
### 2. Service Design & Transformation
- Design service offerings that meet customer needs
- Define service levels and KPIs
- Create service catalogs
- Implement service quality metrics
### 3. Service Transition
- Manage changes effectively
- Implement release management
- Knowledge management practices
- Service validation and testing
### 4. Service Operation
- Incident Management lifecycle
- Problem Management for root cause
- Request Fulfilment
- Event Management and monitoring
- Access Management
### 5. Continual Improvement
- 7-step improvement model
- Process measurement and metrics
- CSI register for improvements
- Value realization tracking
### 6. FinOps for IT Services
- Connect spend to service value
- Unit economics for services
- Right-sizing and optimization
- Cloud and AI cost management
### 7. Communication Bridge
- Executive reporting with SIR (Situation-Impact-Resolution)
- Stakeholder management
- ROI-focused narratives
## Applicability Scenarios
- Implementing ITIL 5 from scratch
- Migrating from ITIL v4 to ITIL 5
- Incident escalation and resolution
- Change management best practices
- Service desk optimization
- IT budget and cost optimization
## References
- [IT Manager's Handbook](./references/it-manager-handbook.md)
- [Management Scenarios](./examples/management-scenarios.md)
- [IT Frameworks Guide](./references/it-management-frameworks.md)
## Limitations
- Strategic advisory only, not legal/financial auditing
- Advice quality depends on provided context
- Always verify against local regulations
FILE:README.de.md
# ITIL 5 Manager (li_itil_manager)
Elite IT-Service-Management-Berater, spezialisiert auf ITSM, FinOps und IT-Governance unter Verwendung des ITIL 5 DPSM-Frameworks.
## Überblick
Ein umfassender ITIL 5-Berater, der Digitale Produkt- und Service-Management (DPSM) mit modernen IT-Management-Praktiken kombiniert. Bietet strategische und operative Anleitung für IT-Manager, Service-Desk-Leiter und digitale Führungskräfte.
## Funktionen
- **ITIL 5 DPSM:** Digitales Produkt- und Service-Management-Ansatz
- **Service-Wertschöpfungskette:** Planen, Engagieren, Gestalten & Transformieren, Beschaffen/Bauen, Liefern & Unterstützen, Verbessern
- **Prozessoptimierung:** Incident-, Problem-, Änderungs-, Wissens- und Serviceanfrage-Management
- **Führungskommunikation:** C-Level Storytelling und ROI-Berichterstattung
- **FinOps-Integration:** Verbindung von Servicekosten mit Geschäftswert
## ITIL 5 Leitprinzipien
1. Auf Wert fokussieren
2. Iterativ vorgehen
3. Zusammenarbeiten und Sichtbarkeit fördern
4. Ganzheitlich denken und arbeiten
5. Einfachheit bewahren
6. Optimieren und automatisieren
7. Alles ist Beziehung
## Verwendung
Auslösen mit Schlüsselwörtern:
- `itil manager`
- `itil5` / `itil 5`
- `it service management`
- `incident management`
- `problem management`
- `change management`
- `service desk`
## Struktur
```
li_itil_manager/
├── SKILL.md
├── README.md
├── references/
│ ├── it-manager-handbook.md
│ └── it-management-frameworks.md
└── examples/
└── management-scenarios.md
```
## Version
- Aktuell: 1.0.0
- Datum: 2026-04-27
- Framework: ITIL 5 DPSM
## Lizenz
Community-Skill - MIT
FILE:README.en.md
# ITIL 5 Manager (li_itil_manager)
Elite IT Service Management Advisor specializing in ITSM, FinOps, and IT governance using ITIL 5 DPSM framework.
## Overview
A comprehensive ITIL 5 advisor combining Digital Product and Service Management (DPSM) with modern IT management practices. Provides strategic and operational guidance for IT managers, service desk leads, and digital leaders.
## Features
- **ITIL 5 DPSM:** Digital Product and Service Management approach
- **Service Value Chain:** Plan, Engage, Design & Transform, Obtain/Build, Deliver & Support, Improve
- **Process Optimization:** Incident, Problem, Change, Knowledge, and Service Request Management
- **Executive Communication:** C-level storytelling and ROI reporting
- **FinOps Integration:** Connecting service cost to business value
## ITIL 5 Guiding Principles
1. Focus on value
2. Progress iteratively
3. Collaborate and promote visibility
4. Think and work holistically
5. Keep it simple
6. Optimize and automate
7. Everything is a relationship
## Usage
Trigger with keywords:
- `itil manager`
- `itil5` / `itil 5`
- `it service management`
- `incident management`
- `problem management`
- `change management`
- `service desk`
## Structure
```
li_itil_manager/
├── SKILL.md # Skill definition
├── README.md # This file
├── references/
│ ├── it-manager-handbook.md
│ └── it-management-frameworks.md
└── examples/
└── management-scenarios.md
```
## Version
- Current: 1.0.0
- Date: 2026-04-27
- Framework: ITIL 5 DPSM
## License
Community skill - MIT
## Author
ClawHub Community
FILE:README.es.md
# ITIL 5 Manager (li_itil_manager)
Asesor élite de gestión de servicios de TI especializado en ITSM, FinOps y gobernanza de TI utilizando el marco ITIL 5 DPSM.
## Descripción
Un asesor completo de ITIL 5 que combina la Gestión de Productos y Servicios Digitales (DPSM) con prácticas modernas de gestión de TI. Proporciona orientación estratégica y operativa para gerentes de TI, líderes de mesa de servicio y líderes digitales.
## Características
- **ITIL 5 DPSM:** Enfoque de Gestión de Productos y Servicios Digitales
- **Cadena de Valor del Servicio:** Planificar, Involucrar, Diseñar y Transformar, Obtener/Construir, Entregar y Apoyar, Mejorar
- **Optimización de Procesos:** Gestión de Incidentes, Problemas, Cambios, Conocimiento y Solicitudes de Servicio
- **Comunicación Ejecutiva:** Narrativa para C-level e informes ROI
- **Integración FinOps:** Conectar costo del servicio con valor empresarial
## Principios Guia ITIL 5
1. Enfocarse en el valor
2. Progresar iterativamente
3. Colaborar y promover visibilidad
4. Pensar y trabajar holísticamente
5. Mantenerlo simple
6. Optimizar y automatizar
7. Todo es una relación
## Uso
Dispara con palabras clave:
- `itil manager`
- `itil5` / `itil 5`
- `it service management`
- `incident management`
- `problem management`
- `change management`
- `service desk`
## Estructura
```
li_itil_manager/
├── SKILL.md
├── README.md
├── references/
│ ├── it-manager-handbook.md
│ └── it-management-frameworks.md
└── examples/
└── management-scenarios.md
```
## Versión
- Actual: 1.0.0
- Fecha: 2026-04-27
- Framework: ITIL 5 DPSM
## Licencia
Habilidad comunitaria - MIT
FILE:README.fr.md
# ITIL 5 Manager (li_itil_manager)
Conseiller Elite en Gestion des Services IT spécialisé en ITSM, FinOps et Gouvernance IT utilisant le framework ITIL 5 DPSM.
## Aperçu
Un conseiller complet ITIL 5 combinant la Gestion des Produits et Services Numériques (DPSM) avec les pratiques modernes de gestion IT. Fournit des orientations stratégiques et opérationnelles pour les responsables IT, les responsables du service desk et les leaders numériques.
## Caractéristiques
- **ITIL 5 DPSM:** Approche de Gestion des Produits et Services Numériques
- **Chaîne de Valeur du Service:** Planifier, Engager, Concevoir et Transformer, Obtenir/Construire, Livrer et Supporter, Améliorer
- **Optimisation des Processus:** Gestion des Incidents, Problèmes, Changements, Connaissances et Demandes de Service
- **Communication Exécutive:** Storytelling pour C-level et rapports ROI
- **Intégration FinOps:** Connecter le coût du service à la valeur métier
## Principes Directeurs ITIL 5
1. Se concentrer sur la valeur
2. Progresser de manière itérative
3. Collaborer et promouvoir la visibilité
4. Penser et travailler holistiquement
5. Garder simple
6. Optimiser et automatiser
7. Tout est une relation
## Utilisation
Déclencher avec mots-clés:
- `itil manager`
- `itil5` / `itil 5`
- `it service management`
- `incident management`
- `problem management`
- `change management`
- `service desk`
## Structure
```
li_itil_manager/
├── SKILL.md
├── README.md
├── references/
│ ├── it-manager-handbook.md
│ └── it-management-frameworks.md
└── examples/
└── management-scenarios.md
```
## Version
- Actuelle: 1.0.0
- Date: 2026-04-27
- Framework: ITIL 5 DPSM
## Licence
Compétence communautaire - MIT
FILE:README.ja.md
# ITIL 5 Manager (li_itil_manager)
ITSM、FinOps、ITガバナンスにおけるエリートITサービス管理アドバイザー。ITIL 5 DPSMフレームワークを専門とします。
## 概要
デジタルプロダクト&サービス管理(DPSM)と最新のIT管理プラクティスを組み合わせた総合的なITIL 5アドバイザー。ITマネージャー、サービスデスクリーダー、デジタルリーダーへの戦略的および運用ガイダンスを提供します。
## 機能
- **ITIL 5 DPSM:** デジタルプロダクト&サービス管理アプローチ
- **サービスバリューチェーン:** プラン、エンゲージ、設計・変革、取得・構築、配信・支援、改善
- **プロセス最適化:** インシデント、問題、変更、ナレッジ、サービスリクエスト管理
- **エグゼクティブコミュニケーション:** CレベルストーリーテリングとROIレポート
- **FinOps統合:** サービスコストとビジネス価値の連携
## ITIL 5指導原則
1. 価値に焦点を当てる
2. 反復的に進捗する
3. コラボレーションと可視性の促進
4. holisticallyに考える
5. シンプルに保つ
6. 最適化と自動化
7. すべてが関係である
## 使用方法
以下のキーワードでトリガー:
- `itil manager`
- `itil5` / `itil 5`
- `it service management`
- `incident management`
- `problem management`
- `change management`
- `service desk`
## 構造
```
li_itil_manager/
├── SKILL.md
├── README.md
├── references/
│ ├── it-manager-handbook.md
│ └── it-management-frameworks.md
└── examples/
└── management-scenarios.md
```
## バージョン
- 現行: 1.0.0
- 日付: 2026-04-27
- フレームワーク: ITIL 5 DPSM
## ライセンス
コミュニティスキル - MIT
FILE:README.ko.md
# ITIL 5 Manager (li_itil_manager)
ITSM, FinOps 및 IT 거버넌스를 전문으로 하는 엘리트 IT 서비스 관리 자문관 ITIL 5 DPSM 프레임워크를 사용합니다.
## 개요
디지털 제품 및 서비스 관리(DPSM)와 최신 IT 관리 관행을 결합한 종합 ITIL 5 자문관입니다. IT 관리자, 서비스 데스크 리더 및 디지털 리더에게 전략적 및 운영 지침을 제공합니다.
## 기능
- **ITIL 5 DPSM:** 디지털 제품 및 서비스 관리 접근 방식
- **서비스 가치 사슬:** 계획, 참여, 설계 및 전환, 획득/구축, 제공 및 지원, 개선
- **프로세스 최적화:** 인시던트, 문제, 변경, 지식 및 서비스 요청 관리
- **임원 커뮤니케이션:** C 레벨 스토리텔링 및 ROI 보고
- **FinOps 통합:** 서비스 비용을 비즈니스 가치에 연결
## ITIL 5 지침 원칙
1. 가치에 집중
2. 반복적으로 진행
3. 협업 및 가시성 촉진
4. 전체적으로 생각하고 작업
5. 단순하게 유지
6. 최적화 및 자동화
7. 모든 것은 관계
## 사용 방법
키워드로 트리거:
- `itil manager`
- `itil5` / `itil 5`
- `it service management`
- `incident management`
- `problem management`
- `change management`
- `service desk`
## 구조
```
li_itil_manager/
├── SKILL.md
├── README.md
├── references/
│ ├── it-manager-handbook.md
│ └── it-management-frameworks.md
└── examples/
└── management-scenarios.md
```
## 버전
- 현재: 1.0.0
- 날짜: 2026-04-27
- 프레임워크: ITIL 5 DPSM
## 라이선스
커뮤니티 스킬 - MIT
FILE:README.md
# ITIL 5 Manager (li_itil_manager)
Elite IT Service Management Advisor specializing in ITSM, FinOps, and IT governance using ITIL 5 DPSM framework.
## Overview
A comprehensive ITIL 5 advisor combining Digital Product and Service Management (DPSM) with modern IT management practices. Provides strategic and operational guidance for IT managers, service desk leads, and digital leaders.
## Features
- **ITIL 5 DPSM:** Digital Product and Service Management approach
- **Service Value Chain:** Plan, Engage, Design & Transform, Obtain/Build, Deliver & Support, Improve
- **Process Optimization:** Incident, Problem, Change, Knowledge, and Service Request Management
- **Executive Communication:** C-level storytelling and ROI reporting
- **FinOps Integration:** Connecting service cost to business value
## ITIL 5 Guiding Principles
1. Focus on value
2. Progress iteratively
3. Collaborate and promote visibility
4. Think and work holistically
5. Keep it simple
6. Optimize and automate
7. Everything is a relationship
## Usage
Trigger with keywords:
- `itil manager`
- `itil5` / `itil 5`
- `it service management`
- `incident management`
- `problem management`
- `change management`
- `service desk`
## Structure
```
li_itil_manager/
├── SKILL.md # Skill definition
├── README.md # This file
├── references/
│ ├── it-manager-handbook.md # IT Management Handbook
│ └── it-management-frameworks.md
└── examples/
└── management-scenarios.md
```
## Version
- Current: 1.0.0
- Date: 2026-04-27
- Framework: ITIL 5 DPSM
## License
Community skill - MIT
## Author
ClawHub Community
FILE:README.pt.md
# ITIL 5 Manager (li_itil_manager)
Assessor Élite de Gerenciamento de Serviços de TI especializado em ITSM, FinOps e Governança de TI usando o framework ITIL 5 DPSM.
## Visão Geral
Um assessor abrangente de ITIL 5 combinando Gerenciamento de Produtos e Serviços Digitais (DPSM) com práticas modernas de gestão de TI. Fornece orientação estratégica e operacional para gerentes de TI, líderes de service desk e líderes digitais.
## Recursos
- **ITIL 5 DPSM:** Abordagem de Gerenciamento de Produtos e Serviços Digitais
- **Cadeia de Valor de Serviço:** Planejar, Engajar, Projetar e Transformar, Obter/Construir, Entregar e Suportar, Melhorar
- **Otimização de Processos:** Gerenciamento de Incidentes, Problemas, Mudanças, Conhecimento e Solicitações de Serviço
- **Comunicação Executiva:** Storytelling para C-level e relatórios de ROI
- **Integração FinOps:** Conectar custo de serviço com valor de negócio
## Princípios Guia ITIL 5
1. Focar no valor
2. Progredir iterativamente
3. Colaborar e promover visibilidade
4. Pensar e trabalhar holísticamente
5. Manter simples
6. Otimizar e automatizar
7. Tudo é uma relação
## Uso
Dispare com palavras-chave:
- `itil manager`
- `itil5` / `itil 5`
- `it service management`
- `incident management`
- `problem management`
- `change management`
- `service desk`
## Estrutura
```
li_itil_manager/
├── SKILL.md
├── README.md
├── references/
│ ├── it-manager-handbook.md
│ └── it-management-frameworks.md
└── examples/
└── management-scenarios.md
```
## Versão
- Atual: 1.0.0
- Data: 2026-04-27
- Framework: ITIL 5 DPSM
## Licença
Habilidade comunitária - MIT
FILE:README.zh-CN.md
# ITIL 5 Manager (li_itil_manager)
精英IT服务管理顾问,专注于使用ITIL 5 DPSM框架的ITSM、FinOps和IT治理。
## 概述
一个综合性的ITIL 5顾问,结合数字产品和服务管理(DPSM)与现代IT管理实践。为IT经理、服务台负责人和数字领导者提供战略和运营指导。
## 功能特点
- **ITIL 5 DPSM:** 数字产品和服务管理方法
- **服务价值链:** 计划、参与、设计与转型、获取/构建、交付与支持、改进
- **流程优化:** 事件、问题、变更、知识和 服务请求管理
- **高管沟通:** C级别故事化叙述和ROI报告
- **FinOps集成:** 连接服务成本与业务价值
## ITIL 5指导原则
1. 聚焦价值
2. 迭代推进
3. 协作并提升透明度
4. 全局思考和工作
5. 保持简洁
6. 优化和自动化
7. 一切都是关系
## 使用方法
使用以下关键词触发:
- `itil manager`
- `itil5` / `itil 5`
- `it service management`
- `incident management`
- `problem management`
- `change management`
- `service desk`
## 目录结构
```
li_itil_manager/
├── SKILL.md # Skill定义
├── README.md # 说明文档
├── references/
│ ├── it-manager-handbook.md # IT管理手册
│ └── it-management-frameworks.md
└── examples/
└── management-scenarios.md
```
## 版本信息
- 当前版本: 1.0.0
- 日期: 2026-04-27
- 框架: ITIL 5 DPSM
## 许可证
社区技能 - MIT
## 作者
ClawHub 社区
FILE:examples/management-scenarios.md
# ITIL Manager Scenarios
Common real-world ITIL management scenarios with expert-driven advice.
## Scenario 1: Implementing ITIL 5 from Scratch
**Situation:** Organization wants to adopt ITIL 5 DPSM approach.
**Expert Advice:**
- Start with ITIL 5 Guiding Principles - focus on value and collaboration
- Map current services to Digital Products
- Identify service relationships with stakeholders
- Implement Service Value Chain activities progressively
- Use "Progress iteratively" - start small, iterate
- **Question:** Would you like deep insights into implementation steps?
## Scenario 2: Major Incident Management
**Situation:** Critical system outage affecting business operations.
**Expert Advice (ITIL 5 Incident Management):**
- **Detect & Log:** Immediate incident creation
- **Categorize & Prioritize:** Impact and urgency assessment
- **Diagnose:** Technical investigation
- **Resolve:** Fix and restore service
- **Close:** Formal closure with customer sign-off
- Communication: Use SIR (Situation-Impact-Resolution) for updates
- Post-incident: Blameless review within 24 hours
- **Question:** Would you like deep insights into escalation procedures?
## Scenario 3: Change Management
**Situation:** Need to deploy major infrastructure change with minimum risk.
**Expert Advice (ITIL 5 Change Management):**
- **RFC:** Complete Request for Change with justification
- **Assessment:** Evaluate risk, impact, and cost
- ** CAB Review:** Present to Change Advisory Board
- **Planning:** Define rollback procedures
- **Implementation:** Execute in change window
- **Review:** Post-implementation review
- Follow "Think and work holistically" - consider all dependencies
- **Question:** Would you like deep insights into risk assessment?
## Scenario 4: Service Desk Optimization
**Situation:** High volume of tickets, low customer satisfaction.
**Expert Advice:**
- Analyze ticket categories and root causes
- Implement Service Request Management for repetitive tasks
- Build Knowledge Base for self-service
- Use "Optimize and automate" - automate routine requests
- Track FCR (First Contact Resolution) and CSAT metrics
- **Question:** Would you like deep insights into KPI optimization?
## Scenario 5: Problem Management
**Situation:** Recurring incidents from underlying root cause.
**Expert Advice:**
- Use Problem Management to find root cause
- Create Problem Record linked to related Incidents
- Analyze trends usingKeppler Incident Analysis
- Implement permanent fix through Change Management
- Update Knowledge Base with workarounds
- **Question:** Would you like deep insights into problem analysis techniques?
## Scenario 6: IT Budget and Cost Optimization
**Situation:** Need to optimize IT spend while maintaining service quality.
**Expert Advice (FinOps + ITIL 5):**
- Map service costs using value chain activities
- Identify under-utilized services
- Implement consumption-based pricing where possible
- Use "Focus on value" - cut low-value services
- Track Cost per Service and Cost per User metrics
- **Question:** Would you like deep insights into FinOps practices?
---
*Reference scenarios for ITIL 5 Manager skill.*
FILE:references/it-management-frameworks.md
# IT Management Frameworks Guide (2026)
Comprehensive guide for aligning IT with business objectives using world-class frameworks.
## 1. IT Governance & Strategy
* **COBIT (Control Objectives for Information and Related Technologies):** Focused on IT corporate governance. Helps align technology with business strategic objectives, manage risks, and ensure regulatory compliance.
* **ISO/IEC 38500:** Provides basic principles for efficient, effective, and acceptable use of IT within organizations, focusing on director responsibilities.
## 2. IT Service Management (ITSM) - ITIL 5
* **ITIL (Information Technology Infrastructure Library):** The global standard for service management. ITIL 5 focuses on the service lifecycle and Digital Product and Service Management (DPSM).
* **ITIL 5 DPSM (Digital Product and Service Management):** New approach treating all IT services as digital products, emphasizing continuous value creation.
* **ISO/IEC 20000:** International standard for IT service management, serving as a basis for organizational quality certifications.
* **MOF (Microsoft Operations Framework):** Adaptation of ITIL practices focused specifically on Microsoft technology ecosystems.
## 3. Enterprise Architecture
* **TOGAF (The Open Group Architecture Framework):** Specialized in designing, planning, and implementing enterprise architectures to ensure technology foundation supports business scalability.
## 4. Project Management & Agile
* **PMBOK (Project Management Body of Knowledge):** Guide for traditional project management (Waterfall/Predictive).
* **PRINCE2 (Projects in Controlled Environments):** Structured method focused on control, organization, and ongoing business justification.
* **Scrum / Agile:** Frameworks for complex project management with focus on rapid, iterative, adaptive delivery.
* **SAFe (Scaled Agile Framework):** Methodology for scaling agile practices in large organizations.
## 5. Security & Risk
* **NIST Cybersecurity Framework:** Guidelines for reducing cybersecurity risks in critical infrastructure and government.
* **ISO/IEC 27001:** International standard for implementing an Information Security Management System (ISMS).
* **FAIR (Factor Analysis of Information Risk):** Quantitative model for understanding and measuring information risk in financial terms.
## 6. Modern Operations & Innovation
* **DevOps Framework:** Full integration between development and operations to accelerate value delivery cycle.
* **SRE (Site Reliability Engineering):** Google's approach using software engineering to solve operations and scalability problems.
* **AIOps:** Use of Artificial Intelligence and Machine Learning to automate incident detection and optimize operational performance.
## Framework Selection Guide
| Need | Recommended Framework |
|------|----------------------|
| IT Governance | COBIT |
| Service Management | ITIL 5 DPSM |
| Enterprise Architecture | TOGAF |
| Traditional Projects | PMBOK/PRINCE2 |
| Agile Projects | Scrum/SAFe |
| Security | ISO 27001/NIST |
| Operations Optimization | DevOps/SRE/AIOps |
FILE:references/it-manager-handbook.md
# IT Manager Handbook (2026 Edition) - ITIL 5 Edition
A strategic reference for managing modern digital technical organizations with ITIL 5 foundation.
## 1. Leadership in a VUCA World
IT Management is now characterized by Volatility, Uncertainty, Complexity, and Ambiguity.
- **Adaptive Strategy:** Move from rigid 5-year plans to "Rolling 12-month Value Roadmaps."
- **Psychological Safety:** The foundation of high-performance engineering teams. Encourage blameless post-mortems and celebrate "smart failures."
- **ITIL 5 Guiding Principles:** Apply "Progress iteratively," "Collaborate and promote visibility," and "Think and work holistically" in leadership approach.
## 2. FinOps 2.0: Value over Cost
Sustainable cloud and AI growth require a FinOps mindset that connects spend to revenue and P&L impact.
- **Unit Economics:** Calculate the "Cost per Transaction" or "Cost per Active AI Agent."
- **Waste Identification:** Historically, 30% of cloud spend is waste. Use AI-driven right-sizing and spot-instance automation.
- **ITIL 5 Service Value Chain:** Use the "Obtain/Build" and "Deliver & Support" practices to optimize technology spend.
## 3. Data-Driven Management (DDM)
Stop making decisions based on intuition or the "Highest Paid Person's Opinion" (HIPPO).
- **Process Mining:** Extract value stream maps from system logs to find actual cycle times and hidden bottlenecks.
- **KPIs that Matter:** Deployment Frequency, Mean Time to Recovery (MTTR), and Service Value Realization (SVR).
- **ITIL 5 Continual Improvement:** Use the 7-step improvement model to drive data-driven optimization.
## 4. AI-Native Governance & Ethics
Governing a symbiotic human-AI workspace where agents are coworkers.
- **Ethical Audit:** Quarterly reviews of AI decision-making bias and algorithmic transparency.
- **Security:** Managing the broad attack surface of LLM integrations and retrieval-augmented generation (RAG) systems.
- **ITIL 5 Risk Management:** Integrate AI governance into the overall service risk management practice.
## 5. ITIL 5 Digital Product and Service Management (DPSM)
### Core Concepts
- **Digital Product (DP):** Any technology-enabled service that delivers value to customers
- **Service Offering:** The totality of how a service supports customer outcomes
- **Service Relationship:** The cooperation between provider and consumer
- **Value Co-creation:** Working with stakeholders to create value
### Service Value Chain Activities
- **Plan:** Define the vision, roadmap, and architecture
- **Engage:** Understand stakeholder needs and expectations
- **Design & Transform:** Create new services and improvements
- **Obtain/Build:** Acquire or develop components and capabilities
- **Deliver & Support:** Service delivery and operational support
- **Improve:** Continual improvement of services
### The 7 Guiding Principles
1. Focus on value
2. Progress iteratively
3. Collaborate and promote visibility
4. Think and work holistically
5. Keep it simple
6. Optimize and automate
7. Everything is a relationship
---
*Reference source for ITIL 5 Manager (li_itil_manager) skill.*劳动法律问题咨询。当用户遇到劳动纠纷、离职补偿、工伤认定、劳动合同问题、加班费争议、社保缴纳、试用期纠纷、裁员赔偿、劳动仲裁流程等问题时触发。技能内含国内七部劳动争议相关法律/条例,并提供两套文书模板。
---
name: labor-law-advisor
description: "劳动法律问题咨询。当用户遇到劳动纠纷、离职补偿、工伤认定、劳动合同问题、加班费争议、社保缴纳、试用期纠纷、裁员赔偿、劳动仲裁流程等问题时触发。技能内含国内七部劳动争议相关法律/条例,并提供两套文书模板。"
version: "1.0.0"
author: "Jeff"
tags: ["labor-law", "legal", "employment", "dispute", "lawyer", "contract"]
---
# 快速开始
直接描述你的劳动问题,例如:
- "公司没签劳动合同,我能要求赔偿吗?"
- "被公司违法辞退,怎么算赔偿金?"
- "工伤认定流程是什么?"
- "帮我写一份劳动仲裁申请书"
- "加班费怎么计算?"
---
## 一、角色与工作流
### 1.1 角色设定
你是一个专业的劳动纠纷解决律师,专注于帮助劳动者维护合法权益。
**回答风格**:
- **严谨**:所有法律观点必须有明确的法条依据,切忌编造
- **务实**:不仅告诉用户"法律怎么说",更要告诉"实际怎么做"
- **审慎**:遇到信息不足时主动询问,不凭假设下结论
### 1.2 工作流
```
信息收集 → 法律检索 → 分析建议
```
**Step 1:信息收集**
如果用户提供的信息不足以做出准确判断,主动询问:
| 信息项 | 询问内容 | 为什么重要 |
|--------|---------|-----------|
| 入职时间 | 何时入职?有无书面劳动合同? | 确定工龄、合同关系 |
| 工资情况 | 月工资多少?工资结构?发放方式? | 计算补偿/赔偿基数 |
| 事件经过 | 发生了什么?有无书面通知/聊天记录/录音? | 定性违法事实、证据评估 |
| 地区 | 工作地在哪个城市? | 最低工资标准、仲裁管辖 |
| 时效 | 知道权益被侵害至今多久了? | 仲裁时效1年 |
**Step 2:法律检索**
- **优先查高频**:先检索 `references/simplified` 中的高频条款(覆盖80%场景,工资支付暂行规定只有完整版)
- **补充查全文**:高频中未找到或需要上下文时,检索 `references/complete/` 中的完整全文
- **网络兜底**:本地未收录或条文不明确时,使用 `web_search` 查询最新司法解释、地方性规定
- **标注来源**:每条法律观点必须标注具体条款(如"《劳动合同法》第47条")
**Step 3:分析建议**
按以下结构输出:
1. **事实梳理**:总结已知事实,标注缺失信息
2. **法律定性**:判断是否违法/违约,引用具体条款
3. **权益计算**:如有赔偿/补偿,给出具体计算方式和金额
4. **落地建议**:下一步该做什么、准备什么材料、注意什么时效
5. **风险提示**:用户可能忽略的风险点(如证据不足、时效已过、地区差异)
---
## 二、回答规范
### 2.1 必须遵守
| 原则 | 说明 |
|------|------|
| 法条必须真实 | 引用的法律条文必须来自本地 references 或经 web_search 验证,严禁编造 |
| 不确定就询问 | 关键信息缺失时(如入职时间、工资数额、地区),先询问再下结论 |
| 区分原则与实践 | 法律条文是一回事,实际执行可能因地区、证据情况而不同,需同时说明 |
| 计算必须透明 | 赔偿/补偿金额的计算过程要完整展示,让用户知道数字怎么来的 |
### 2.2 禁止行为
- ❌ 编造不存在的法律条文
- ❌ 在信息不足时给出确定性结论
- ❌ 只讲法律不讲操作步骤
- ❌ 忽略时效、证据等风险提示
---
## 三、法律体系(分层存储,全部来自于https://flk.npc.gov.cn/index)
本 skill 采用**分层存储**设计,平衡响应速度与查询完整性:
### 3.1 高频条款(simplified目录)
**默认加载**,覆盖80%常见咨询场景,约占总条文40%:
| 法律/司法解释 | 文件 | 核心场景 |
|-------------|------|---------|
| 《劳动合同法》 | `references/simplified/劳动合同法.md` | 合同订立、履行、解除、终止、赔偿 |
| 《劳动合同法实施条例》 | `references/simplified/劳动合同法实施条例.md` | 双倍工资、经济补偿计算、劳务派遣 |
| 《劳动争议调解仲裁法》 | `references/simplified/劳动争议调解仲裁法.md` | 仲裁时效、管辖、程序、证据 |
| 《工伤保险条例》 | `references/simplified/工伤保险条例.md` | 工伤认定、劳动能力鉴定、待遇 |
| 劳动争议司法解释(一) | `references/simplified/司法解释(一).md` | 受理范围、管辖、仲裁与诉讼衔接、举证责任 |
| 劳动争议司法解释(二) | `references/simplified/司法解释(二).md` | 二倍工资、无固定期限合同、竞业限制、社保约定无效 |
### 3.2 完整全文(complete目录)
**按需加载**,当用户询问非高频常见条款、需要补充相关法律的相关法条或需要上下文时读取:
| 法律/司法解释 | 文件 |
|-------------|------|
| 《劳动合同法》 | `references/complete/劳动合同法.md` |
| 《劳动合同法实施条例》 | `references/complete/劳动合同法实施条例.md` |
| 《劳动争议调解仲裁法》 | `references/complete/劳动争议调解仲裁法.md` |
| 《工伤保险条例》 | `references/complete/工伤保险条例.md` |
| 《工资支付暂行规定》 | `references/complete/工资支付暂行规定.md` |
| 劳动争议司法解释(一) | `references/complete/司法解释(一).md` |
| 劳动争议司法解释(二) | `references/complete/司法解释(二).md` |
### 3.3 加载规则
```
用户提问
│
▼
┌─────────────────────┐
│ 先查simplified目录高频条款 │ ← 默认加载,覆盖80%场景
└─────────────────────┘
│
├─ 找到目标条款 → 引用回答
│
└─ 未找到/需要上下文 → 查 complete/ 完整全文
│
└─ 补充引用回答
```
---
## 四、高频场景速查
### 4.1 未签劳动合同
| 项目 | 内容 |
|------|------|
| 法律依据 | 劳动合同法第10条、第82条;实施条例第5-7条 |
| 权利 | 用工超1个月不满1年 → 每月2倍工资;满1年 → 视为无固定期限合同 |
| 时效 | 从离职之日起1年内申请仲裁 |
### 4.2 违法解除/终止合同
| 项目 | 内容 |
|------|------|
| 法律依据 | 劳动合同法第47条、第48条、第87条 |
| 赔偿金 | 经济补偿 × 2 |
| 经济补偿计算 | N = 工作年限(满1年=1个月,6月以上不满1年按1年,不满6月=0.5个月) |
### 4.3 离职补偿(N、N+1、2N)
| 类型 | 适用情形 | 法律依据 |
|------|---------|---------|
| N | 协商解除、合同到期不续(单位不续)、经济性裁员等 | 第46条 |
| N+1 | 单位提前30日通知或额外支付1个月工资解除 | 第40条 |
| 2N | 单位违法解除/终止 | 第87条 |
### 4.4 加班费
**法律依据**:
- 《劳动法》第44条:安排劳动者延长工作时间的,支付不低于工资的150%的工资报酬;休息日安排劳动者工作又不能安排补休的,支付不低于工资的200%的工资报酬;法定休假日安排劳动者工作的,支付不低于工资的300%的工资报酬。
- 《工资支付暂行规定》(劳部发〔1994〕489号)第13条:用人单位在劳动者完成劳动定额或规定的工作任务后,根据实际需要安排劳动者在法定标准工作时间以外工作的,应按以下标准支付工资。
**倍数规定(全国统一)**:
| 加班情形 | 倍数 | 能否调休替代 |
|---------|------|-------------|
| 工作日延长工作时间(平时加班) | 1.5倍 | 否,必须支付 |
| 休息日加班(周末) | 2倍 | 可以安排补休,不能补休的支付2倍 |
| 法定节假日加班 | 3倍 | 否,必须支付,且不得以调休替代 |
**关键概念:倍数是"相乘得总计"**
- 工作日加班1.5倍 = 正常工资 + 额外0.5倍加班费
- 休息日加班2倍 = 正常工资 + 额外1倍加班费
- 法定节假日3倍 = 正常工资 + 额外2倍加班费
**举例**:月工资6000元,日工资=6000÷21.75=275.86元
- 工作日加班8小时:275.86 × 1.5 = 413.79元(包含275.86正常工资,额外137.93是加班费)
- 休息日加班、法定节假日加班同理
**计算基数(地区差异大,需核实当地口径)**:
| 地区 | 计算基数规则 |
|------|-------------|
| 北京 | 原则上按劳动合同约定工资;无约定的按实际工资 |
| 上海 | 按正常出勤月工资的70%为基数(剔除加班工资) |
| 广东 | 按实际月工资(剔除非常规发放的奖金、津贴) |
| 江苏 | 按劳动合同约定的工资;无约定的按实际月平均工资 |
> ⚠️ **注意**:计算基数不得低于当地最低工资标准。各地对"工资"是否包含绩效、奖金、津贴等口径不同,建议查询当地规定或咨询当地仲裁委。
### 4.5 工伤认定
| 项目 | 内容 |
|------|------|
| 申请时限 | 单位30日内 → 职工/亲属/工会1年内 |
| 认定机构 | 用人单位所在地统筹地区社会保险行政部门 |
| 待遇 | 医疗费、停工留薪期工资、一次性伤残补助金等(工伤保险条例第30-37条) |
### 4.6 劳动仲裁流程
| 步骤 | 内容 |
|------|------|
| 1. 准备材料 | 仲裁申请书、身份证复印件、证据材料 |
| 2. 提交申请 | 劳动合同履行地或用人单位所在地仲裁委 |
| 3. 受理 | 5日内决定是否受理 |
| 4. 开庭 | 受理后45日内结案,复杂可延15日 |
| 5. 裁决 | 不服可15日内向法院起诉 |
| 时效 | 知道或应当知道权利被侵害之日起1年 |
### 4.7 被迫解除劳动合同(拿补偿走人)
| 项目 | 内容 |
|------|------|
| 法律依据 | 劳动合同法第38条 |
| 常见情形 | 拖欠工资、未缴社保、未提供劳动保护、规章制度违法等 |
| 操作 | 书面通知单位,说明解除理由,保留送达证据 |
---
## 五、文书模板(先询问是否需要)
### 5.1 被迫解除劳动合同通知书
```
被迫解除劳动合同通知书
致:[用人单位全称]
本人 [姓名],身份证号 [号码],于 [入职日期] 入职贵单位,担任 [岗位]。
因贵单位存在以下违法行为:
□ 未及时足额支付劳动报酬(具体:[拖欠金额及月份])
□ 未依法为本人缴纳社会保险费
□ 未按照劳动合同约定提供劳动保护或劳动条件
□ 其他:[具体说明]
依据《中华人民共和国劳动合同法》第三十八条之规定,本人现通知贵单位:
自本通知书送达之日起,解除双方劳动合同关系。
请贵单位于接到本通知后 [X] 日内:
1. 为本人办理离职手续,出具解除劳动合同证明;
2. 支付拖欠的劳动报酬共计 [金额] 元;
3. 支付经济补偿金共计 [金额] 元([工作年限] 年 × [月平均工资] 元);
4. 补缴社会保险费。
特此通知。
通知人:[签名]
日期:[日期]
送达方式:□ EMS邮寄(单号:) □ 当面送达(见证人:) □ 电子邮件
```
### 5.2 劳动仲裁申请书(简要版)
```
劳动仲裁申请书
申请人:[姓名],性别,身份证号,住址,电话
被申请人:[单位全称],统一社会信用代码,地址,法定代表人
仲裁请求:
1. 请求裁决被申请人支付 [具体请求,如:违法解除劳动合同赔偿金 X 元]
2. 请求裁决被申请人支付 [拖欠工资 X 元]
3. ...
事实与理由:
[入职时间、岗位、合同情况、事件经过、单位违法行为、法律依据]
此致
[XX 劳动人事争议仲裁委员会]
申请人:[签名]
日期:[日期]
附:证据清单
1. 劳动合同
2. 工资流水
3. 解除通知
4. ...
```
---
## 六、需实时查询的数据
以下信息因**各地标准不同**或**每年调整**,不适合放入本地缓存,回答时需先给出法律依据和通用原则,再主动提供 `web_search` 帮助:
| 信息类型 | 法律依据 | 查询建议 |
|---------|---------|---------|
| **最低工资标准** | 《劳动法》第48条:国家实行最低工资保障制度,具体标准由省、自治区、直辖市人民政府规定 | 搜索"[城市名] 2025年最低工资标准" |
| **社会平均工资(社平工资)** | 用于计算经济补偿金上限、社保缴费基数 | 搜索"[城市名] 2025年度社平工资"(通常用上年度数据) |
| **加班费计算基数口径** | 《劳动法》第44条、《工资支付暂行规定》第13条:按劳动合同规定的劳动者本人小时/日工资标准支付。具体"工资"界定口径各地不同 | 各地裁判口径差异大,建议搜索"[城市名] 加班费计算基数" |
| **工资支付时间** | 《工资支付暂行规定》第7条:工资必须在约定日期支付,至少每月支付一次 | 搜索"[城市名] 工资支付条例"(部分省市有地方性规定) |
| **工资代扣限制** | 《工资支付暂行规定》第15-16条:代扣仅限个税、社保、法院判决费用;因劳动者原因造成损失每月扣除不超过20% | 通用规定,无需额外查询 |
| **劳动仲裁委联系方式** | 《劳动争议调解仲裁法》第17条:劳动争议仲裁委员会按照统筹规划、合理布局和适应实际需要的原则设立 | 搜索"[城市名] 劳动人事争议仲裁委员会 地址 电话" |
| **当地工伤认定机构** | 《工伤保险条例》第17条:用人单位所在地统筹地区社会保险行政部门 | 搜索"[城市名] 工伤认定 社保行政部门" |
---
## 七、外部查询兜底
当用户问题涉及本 skill 未收录的法律(如《劳动法》《社会保险法》《女职工劳动保护特别规定》等)时:
- 告知用户该法律不在本地缓存中
- 提供已知的通用原则
- 建议用户通过 `web_search` 或国家法律法规数据库(flk.npc.gov.cn)查询最新条文
---
## 八、重要提示(免责声明)
1. **本 skill 提供的是法律信息参考,不构成正式法律意见**
2. **证据为王**:聊天记录、工资条、考勤记录、邮件、录音都是关键证据
3. **时效优先**:劳动仲裁时效为1年,从知道权利被侵害之日起算
4. **地区差异**:各地对具体标准(如社平工资、最低工资标准)有差异,计算时需注明当地标准
5. **最新修订**:法律可能修订,重要案件建议核实最新条文或咨询专业律师
FILE:README.md
# labor-law-advisor
中国劳动法律咨询技能,专为 Agent 设计。
## 功能
- 劳动纠纷法律分析与权益计算(N / N+1 / 2N 赔偿金)
- 覆盖试用期纠纷、违法辞退、拖欠工资、加班费、工伤认定等高频场景
- 内置 7 部劳动争议相关法律/司法解释,按需加载
- 生成「被迫解除劳动合同通知书」和「劳动仲裁申请书」两套文书模板
- 劳动仲裁全流程指导(材料准备、管辖、时效、举证)
## 内置法律法规
| 法律/司法解释 | 完整版 | 高频精简版 |
|-------------|--------|-----------|
| 《劳动合同法》 | ✅ | ✅ |
| 《劳动合同法实施条例》 | ✅ | ✅ |
| 《劳动争议调解仲裁法》 | ✅ | ✅ |
| 《工伤保险条例》 | ✅ | ✅ |
| 《工资支付暂行规定》 | ✅ | - |
| 劳动争议司法解释(一) | ✅ | ✅ |
| 劳动争议司法解释(二) | ✅ | ✅ |
## 安装
### 方式一:从 SkillHub 安装(推荐)
在 https://skillhub.cn 中或 WorkBuddy 技能中搜索 "labor-law-advisor" 或 "劳动法" 即可找到并安装。
### 方式二:从 GitHub 克隆
```bash
git clone https://github.com/xunx33/labor-law-advisor.git
cp -r labor-law-advisor ~/.openclaw/skills/labor-law-advisor
```
### 方式三:手动安装
1. 下载本仓库的 zip 文件
2. 解压到 `~/.openclaw/skills/labor-law-advisor/`
3. 确保目录结构如下:
```
labor-law-advisor/
├── SKILL.md
├── _meta.json
├── LICENSE
├── README.md
└── references/
├── complete/
└── simplified/
```
## 使用方式
安装后,直接用自然语言描述你的劳动问题,例如:
- "公司没签劳动合同,我能要求赔偿吗?"
- "试用期被辞退,公司说我不符合录用条件,怎么办?"
- "被公司违法辞退,怎么算赔偿金?"
- "工伤认定流程是什么?"
- "帮我写一份劳动仲裁申请书"
## 免责声明
本技能提供法律信息参考,不构成正式法律意见。重要案件建议咨询专业律师。
法律文本来源于中国法律法规公开数据库,仅供学习参考。
## 许可证
[MIT License](LICENSE)
FILE:references/complete/劳动争议调解仲裁法.md
# 中华人民共和国劳动争议调解仲裁法(完整版)
中华人民共和国劳动争议调解仲裁法
(2007年12月29日第十届全国人民代表大会常务委员会第三十一次会议通过)
目 录
## 第一章 总 则
## 第二章 调 解
## 第三章 仲 裁
第一节 一般规定
第二节 申请和受理
第三节 开庭和裁决
## 第四章 附 则
## 第一章 总 则
**第一条 为了公正及时解决劳动争议,保护当事人合法权益,促进劳动关系和谐稳定,制定本法。**
**第二条 中华人民共和国境内的用人单位与劳动者发生的下列劳动争议,适用本法:**
(一)因确认劳动关系发生的争议;
(二)因订立、履行、变更、解除和终止劳动合同发生的争议;
(三)因除名、辞退和辞职、离职发生的争议;
(四)因工作时间、休息休假、社会保险、福利、培训以及劳动保护发生的争议;
(五)因劳动报酬、工伤医疗费、经济补偿或者赔偿金等发生的争议;
(六)法律、法规规定的其他劳动争议。
**第三条 解决劳动争议,应当根据事实,遵循合法、公正、及时、着重调解的原则,依法保护当事人的合法权益。**
**第四条 发生劳动争议,劳动者可以与用人单位协商,也可以请工会或者第三方共同与用人单位协商,达成和解协议。**
**第五条 发生劳动争议,当事人不愿协商、协商不成或者达成和解协议后不履行的,可以向调解组织申请调解;不愿调解、调解不成或者达成调解协议后不履行的,可以向劳动争议仲裁委员会申请仲裁;对仲裁裁决不服的,除本法另有规定的外,可以向人民法院提起诉讼。**
**第六条 发生劳动争议,当事人对自己提出的主张,有责任提供证据。与争议事项有关的证据属于用人单位掌握管理的,用人单位应当提供;用人单位不提供的,应当承担不利后果。**
**第七条 发生劳动争议的劳动者一方在十人以上,并有共同请求的,可以推举代表参加调解、仲裁或者诉讼活动。**
**第八条 县级以上人民政府劳动行政部门会同工会和企业方面代表建立协调劳动关系三方机制,共同研究解决劳动争议的重大问题。**
**第九条 用人单位违反国家规定,拖欠或者未足额支付劳动报酬,或者拖欠工伤医疗费、经济补偿或者赔偿金的,劳动者可以向劳动行政部门投诉,劳动行政部门应当依法处理。**
## 第二章 调 解
**第十条 发生劳动争议,当事人可以到下列调解组织申请调解:**
(一)企业劳动争议调解委员会;
(二)依法设立的基层人民调解组织;
(三)在乡镇、街道设立的具有劳动争议调解职能的组织。
企业劳动争议调解委员会由职工代表和企业代表组成。职工代表由工会成员担任或者由全体职工推举产生,企业代表由企业负责人指定。企业劳动争议调解委员会主任由工会成员或者双方推举的人员担任。
**第十一条 劳动争议调解组织的调解员应当由公道正派、联系群众、热心调解工作,并具有一定法律知识、政策水平和文化水平的成年公民担任。**
**第十二条 当事人申请劳动争议调解可以书面申请,也可以口头申请。口头申请的,调解组织应当当场记录申请人基本情况、申请调解的争议事项、理由和时间。**
**第十三条 调解劳动争议,应当充分听取双方当事人对事实和理由的陈述,耐心疏导,帮助其达成协议。**
**第十四条 经调解达成协议的,应当制作调解协议书。**
调解协议书由双方当事人签名或者盖章,经调解员签名并加盖调解组织印章后生效,对双方当事人具有约束力,当事人应当履行。
自劳动争议调解组织收到调解申请之日起十五日内未达成调解协议的,当事人可以依法申请仲裁。
**第十五条 达成调解协议后,一方当事人在协议约定期限内不履行调解协议的,另一方当事人可以依法申请仲裁。**
**第十六条 因支付拖欠劳动报酬、工伤医疗费、经济补偿或者赔偿金事项达成调解协议,用人单位在协议约定期限内不履行的,劳动者可以持调解协议书依法向人民法院申请支付令。人民法院应当依法发出支付令。**
## 第三章 仲 裁
第一节 一般规定
**第十七条 劳动争议仲裁委员会按照统筹规划、合理布局和适应实际需要的原则设立。省、自治区人民政府可以决定在市、县设立;直辖市人民政府可以决定在区、县设立。直辖市、设区的市也可以设立一个或者若干个劳动争议仲裁委员会。劳动争议仲裁委员会不按行政区划层层设立。**
**第十八条 国务院劳动行政部门依照本法有关规定制定仲裁规则。省、自治区、直辖市人民政府劳动行政部门对本行政区域的劳动争议仲裁工作进行指导。**
**第十九条 劳动争议仲裁委员会由劳动行政部门代表、工会代表和企业方面代表组成。劳动争议仲裁委员会组成人员应当是单数。**
劳动争议仲裁委员会依法履行下列职责:
(一)聘任、解聘专职或者兼职仲裁员;
(二)受理劳动争议案件;
(三)讨论重大或者疑难的劳动争议案件;
(四)对仲裁活动进行监督。
劳动争议仲裁委员会下设办事机构,负责办理劳动争议仲裁委员会的日常工作。
**第二十条 劳动争议仲裁委员会应当设仲裁员名册。**
仲裁员应当公道正派并符合下列条件之一:
(一)曾任审判员的;
(二)从事法律研究、教学工作并具有中级以上职称的;
(三)具有法律知识、从事人力资源管理或者工会等专业工作满五年的;
(四)律师执业满三年的。
**第二十一条 劳动争议仲裁委员会负责管辖本区域内发生的劳动争议。**
劳动争议由劳动合同履行地或者用人单位所在地的劳动争议仲裁委员会管辖。双方当事人分别向劳动合同履行地和用人单位所在地的劳动争议仲裁委员会申请仲裁的,由劳动合同履行地的劳动争议仲裁委员会管辖。
**第二十二条 发生劳动争议的劳动者和用人单位为劳动争议仲裁案件的双方当事人。**
劳务派遣单位或者用工单位与劳动者发生劳动争议的,劳务派遣单位和用工单位为共同当事人。
**第二十三条 与劳动争议案件的处理结果有利害关系的第三人,可以申请参加仲裁活动或者由劳动争议仲裁委员会通知其参加仲裁活动。**
**第二十四条 当事人可以委托代理人参加仲裁活动。委托他人参加仲裁活动,应当向劳动争议仲裁委员会提交有委托人签名或者盖章的委托书,委托书应当载明委托事项和权限。**
**第二十五条 丧失或者部分丧失民事行为能力的劳动者,由其法定代理人代为参加仲裁活动;无法定代理人的,由劳动争议仲裁委员会为其指定代理人。劳动者死亡的,由其近亲属或者代理人参加仲裁活动。**
**第二十六条 劳动争议仲裁公开进行,但当事人协议不公开进行或者涉及国家秘密、商业秘密和个人隐私的除外。**
第二节 申请和受理
**第二十七条 劳动争议申请仲裁的时效期间为一年。仲裁时效期间从当事人知道或者应当知道其权利被侵害之日起计算。**
前款规定的仲裁时效,因当事人一方向对方当事人主张权利,或者向有关部门请求权利救济,或者对方当事人同意履行义务而中断。从中断时起,仲裁时效期间重新计算。
因不可抗力或者有其他正当理由,当事人不能在本条第一款规定的仲裁时效期间申请仲裁的,仲裁时效中止。从中止时效的原因消除之日起,仲裁时效期间继续计算。
劳动关系存续期间因拖欠劳动报酬发生争议的,劳动者申请仲裁不受本条第一款规定的仲裁时效期间的限制;但是,劳动关系终止的,应当自劳动关系终止之日起一年内提出。
**第二十八条 申请人申请仲裁应当提交书面仲裁申请,并按照被申请人人数提交副本。**
仲裁申请书应当载明下列事项:
(一)劳动者的姓名、性别、年龄、职业、工作单位和住所,用人单位的名称、住所和法定代表人或者主要负责人的姓名、职务;
(二)仲裁请求和所根据的事实、理由;
(三)证据和证据来源、证人姓名和住所。
书写仲裁申请确有困难的,可以口头申请,由劳动争议仲裁委员会记入笔录,并告知对方当事人。
**第二十九条 劳动争议仲裁委员会收到仲裁申请之日起五日内,认为符合受理条件的,应当受理,并通知申请人;认为不符合受理条件的,应当书面通知申请人不予受理,并说明理由。对劳动争议仲裁委员会不予受理或者逾期未作出决定的,申请人可以就该劳动争议事项向人民法院提起诉讼。**
**第三十条 劳动争议仲裁委员会受理仲裁申请后,应当在五日内将仲裁申请书副本送达被申请人。**
被申请人收到仲裁申请书副本后,应当在十日内向劳动争议仲裁委员会提交答辩书。劳动争议仲裁委员会收到答辩书后,应当在五日内将答辩书副本送达申请人。被申请人未提交答辩书的,不影响仲裁程序的进行。
第三节 开庭和裁决
**第三十一条 劳动争议仲裁委员会裁决劳动争议案件实行仲裁庭制。仲裁庭由三名仲裁员组成,设首席仲裁员。简单劳动争议案件可以由一名仲裁员独任仲裁。**
**第三十二条 劳动争议仲裁委员会应当在受理仲裁申请之日起五日内将仲裁庭的组成情况书面通知当事人。**
**第三十三条 仲裁员有下列情形之一,应当回避,当事人也有权以口头或者书面方式提出回避申请:**
(一)是本案当事人或者当事人、代理人的近亲属的;
(二)与本案有利害关系的;
(三)与本案当事人、代理人有其他关系,可能影响公正裁决的;
(四)私自会见当事人、代理人,或者接受当事人、代理人的请客送礼的。
劳动争议仲裁委员会对回避申请应当及时作出决定,并以口头或者书面方式通知当事人。
**第三十四条 仲裁员有本法第三十三条第四项规定情形,或者有索贿受贿、徇私舞弊、枉法裁决行为的,应当依法承担法律责任。劳动争议仲裁委员会应当将其解聘。**
**第三十五条 仲裁庭应当在开庭五日前,将开庭日期、地点书面通知双方当事人。当事人有正当理由的,可以在开庭三日前请求延期开庭。是否延期,由劳动争议仲裁委员会决定。**
**第三十六条 申请人收到书面通知,无正当理由拒不到庭或者未经仲裁庭同意中途退庭的,可以视为撤回仲裁申请。**
被申请人收到书面通知,无正当理由拒不到庭或者未经仲裁庭同意中途退庭的,可以缺席裁决。
**第三十七条 仲裁庭对专门性问题认为需要鉴定的,可以交由当事人约定的鉴定机构鉴定;当事人没有约定或者无法达成约定的,由仲裁庭指定的鉴定机构鉴定。**
根据当事人的请求或者仲裁庭的要求,鉴定机构应当派鉴定人参加开庭。当事人经仲裁庭许可,可以向鉴定人提问。
**第三十八条 当事人在仲裁过程中有权进行质证和辩论。质证和辩论终结时,首席仲裁员或者独任仲裁员应当征询当事人的最后意见。**
**第三十九条 当事人提供的证据经查证属实的,仲裁庭应当将其作为认定事实的根据。**
劳动者无法提供由用人单位掌握管理的与仲裁请求有关的证据,仲裁庭可以要求用人单位在指定期限内提供。用人单位在指定期限内不提供的,应当承担不利后果。
**第四十条 仲裁庭应当将开庭情况记入笔录。当事人和其他仲裁参加人认为对自己陈述的记录有遗漏或者差错的,有权申请补正。如果不予补正,应当记录该申请。**
笔录由仲裁员、记录人员、当事人和其他仲裁参加人签名或者盖章。
**第四十一条 当事人申请劳动争议仲裁后,可以自行和解。达成和解协议的,可以撤回仲裁申请。**
**第四十二条 仲裁庭在作出裁决前,应当先行调解。**
调解达成协议的,仲裁庭应当制作调解书。
调解书应当写明仲裁请求和当事人协议的结果。调解书由仲裁员签名,加盖劳动争议仲裁委员会印章,送达双方当事人。调解书经双方当事人签收后,发生法律效力。
调解不成或者调解书送达前,一方当事人反悔的,仲裁庭应当及时作出裁决。
**第四十三条 仲裁庭裁决劳动争议案件,应当自劳动争议仲裁委员会受理仲裁申请之日起四十五日内结束。案情复杂需要延期的,经劳动争议仲裁委员会主任批准,可以延期并书面通知当事人,但是延长期限不得超过十五日。逾期未作出仲裁裁决的,当事人可以就该劳动争议事项向人民法院提起诉讼。**
仲裁庭裁决劳动争议案件时,其中一部分事实已经清楚,可以就该部分先行裁决。
**第四十四条 仲裁庭对追索劳动报酬、工伤医疗费、经济补偿或者赔偿金的案件,根据当事人的申请,可以裁决先予执行,移送人民法院执行。**
仲裁庭裁决先予执行的,应当符合下列条件:
(一)当事人之间权利义务关系明确;
(二)不先予执行将严重影响申请人的生活。
劳动者申请先予执行的,可以不提供担保。
**第四十五条 裁决应当按照多数仲裁员的意见作出,少数仲裁员的不同意见应当记入笔录。仲裁庭不能形成多数意见时,裁决应当按照首席仲裁员的意见作出。**
**第四十六条 裁决书应当载明仲裁请求、争议事实、裁决理由、裁决结果和裁决日期。裁决书由仲裁员签名,加盖劳动争议仲裁委员会印章。对裁决持不同意见的仲裁员,可以签名,也可以不签名。**
**第四十七条 下列劳动争议,除本法另有规定的外,仲裁裁决为终局裁决,裁决书自作出之日起发生法律效力:**
(一)追索劳动报酬、工伤医疗费、经济补偿或者赔偿金,不超过当地月最低工资标准十二个月金额的争议;
(二)因执行国家的劳动标准在工作时间、休息休假、社会保险等方面发生的争议。
**第四十八条 劳动者对本法第四十七条规定的仲裁裁决不服的,可以自收到仲裁裁决书之日起十五日内向人民法院提起诉讼。**
**第四十九条 用人单位有证据证明本法第四十七条规定的仲裁裁决有下列情形之一,可以自收到仲裁裁决书之日起三十日内向劳动争议仲裁委员会所在地的中级人民法院申请撤销裁决:**
(一)适用法律、法规确有错误的;
(二)劳动争议仲裁委员会无管辖权的;
(三)违反法定程序的;
(四)裁决所根据的证据是伪造的;
(五)对方当事人隐瞒了足以影响公正裁决的证据的;
(六)仲裁员在仲裁该案时有索贿受贿、徇私舞弊、枉法裁决行为的。
人民法院经组成合议庭审查核实裁决有前款规定情形之一的,应当裁定撤销。
仲裁裁决被人民法院裁定撤销的,当事人可以自收到裁定书之日起十五日内就该劳动争议事项向人民法院提起诉讼。
**第五十条 当事人对本法第四十七条规定以外的其他劳动争议案件的仲裁裁决不服的,可以自收到仲裁裁决书之日起十五日内向人民法院提起诉讼;期满不起诉的,裁决书发生法律效力。**
**第五十一条 当事人对发生法律效力的调解书、裁决书,应当依照规定的期限履行。一方当事人逾期不履行的,另一方当事人可以依照民事诉讼法的有关规定向人民法院申请执行。受理申请的人民法院应当依法执行。**
## 第四章 附 则
**第五十二条 事业单位实行聘用制的工作人员与本单位发生劳动争议的,依照本法执行;法律、行政法规或者国务院另有规定的,依照其规定。**
**第五十三条 劳动争议仲裁不收费。劳动争议仲裁委员会的经费由财政予以保障。**
**第五十四条 本法自2008年5月1日起施行。**
FILE:references/complete/劳动合同法.md
# 中华人民共和国劳动合同法(完整版)
中华人民共和国劳动合同法
(2007年6月29日第十届全国人民代表大会常务委员会第二十八次会议通过 根据2012年12月28日第十一届全国人民代表大会常务委员会第三十次会议《关于修改〈中华人民共和国劳动合同法〉的决定》修正)
目 录
## 第一章 总 则
## 第二章 劳动合同的订立
## 第三章 劳动合同的履行和变更
## 第四章 劳动合同的解除和终止
## 第五章 特别规定
第一节 集体合同
第二节 劳务派遣
第三节 非全日制用工
## 第六章 监督检查
## 第七章 法律责任
## 第八章 附 则
## 第一章 总 则
**第一条 为了完善劳动合同制度,明确劳动合同双方当事人的权利和义务,保护劳动者的合法权益,构建和发展和谐稳定的劳动关系,制定本法。**
**第二条 中华人民共和国境内的企业、个体经济组织、民办非企业单位等组织(以下称用人单位)与劳动者建立劳动关系,订立、履行、变更、解除或者终止劳动合同,适用本法。**
国家机关、事业单位、社会团体和与其建立劳动关系的劳动者,订立、履行、变更、解除或者终止劳动合同,依照本法执行。
**第三条 订立劳动合同,应当遵循合法、公平、平等自愿、协商一致、诚实信用的原则。**
依法订立的劳动合同具有约束力,用人单位与劳动者应当履行劳动合同约定的义务。
**第四条 用人单位应当依法建立和完善劳动规章制度,保障劳动者享有劳动权利、履行劳动义务。**
用人单位在制定、修改或者决定有关劳动报酬、工作时间、休息休假、劳动安全卫生、保险福利、职工培训、劳动纪律以及劳动定额管理等直接涉及劳动者切身利益的规章制度或者重大事项时,应当经职工代表大会或者全体职工讨论,提出方案和意见,与工会或者职工代表平等协商确定。
在规章制度和重大事项决定实施过程中,工会或者职工认为不适当的,有权向用人单位提出,通过协商予以修改完善。
用人单位应当将直接涉及劳动者切身利益的规章制度和重大事项决定公示,或者告知劳动者。
**第五条 县级以上人民政府劳动行政部门会同工会和企业方面代表,建立健全协调劳动关系三方机制,共同研究解决有关劳动关系的重大问题。**
**第六条 工会应当帮助、指导劳动者与用人单位依法订立和履行劳动合同,并与用人单位建立集体协商机制,维护劳动者的合法权益。**
## 第二章 劳动合同的订立
**第七条 用人单位自用工之日起即与劳动者建立劳动关系。用人单位应当建立职工名册备查。**
**第八条 用人单位招用劳动者时,应当如实告知劳动者工作内容、工作条件、工作地点、职业危害、安全生产状况、劳动报酬,以及劳动者要求了解的其他情况;用人单位有权了解劳动者与劳动合同直接相关的基本情况,劳动者应当如实说明。**
**第九条 用人单位招用劳动者,不得扣押劳动者的居民身份证和其他证件,不得要求劳动者提供担保或者以其他名义向劳动者收取财物。**
**第十条 建立劳动关系,应当订立书面劳动合同。**
已建立劳动关系,未同时订立书面劳动合同的,应当自用工之日起一个月内订立书面劳动合同。
用人单位与劳动者在用工前订立劳动合同的,劳动关系自用工之日起建立。
**第十一条 用人单位未在用工的同时订立书面劳动合同,与劳动者约定的劳动报酬不明确的,新招用的劳动者的劳动报酬按照集体合同规定的标准执行;没有集体合同或者集体合同未规定的,实行同工同酬。**
**第十二条 劳动合同分为固定期限劳动合同、无固定期限劳动合同和以完成一定工作任务为期限的劳动合同。**
**第十三条 固定期限劳动合同,是指用人单位与劳动者约定合同终止时间的劳动合同。**
用人单位与劳动者协商一致,可以订立固定期限劳动合同。
**第十四条 无固定期限劳动合同,是指用人单位与劳动者约定无确定终止时间的劳动合同。**
用人单位与劳动者协商一致,可以订立无固定期限劳动合同。有下列情形之一,劳动者提出或者同意续订、订立劳动合同的,除劳动者提出订立固定期限劳动合同外,应当订立无固定期限劳动合同:
(一)劳动者在该用人单位连续工作满十年的;
(二)用人单位初次实行劳动合同制度或者国有企业改制重新订立劳动合同时,劳动者在该用人单位连续工作满十年且距法定退休年龄不足十年的;
(三)连续订立二次固定期限劳动合同,且劳动者没有本法第三十九条和第四十条第一项、第二项规定的情形,续订劳动合同的。
用人单位自用工之日起满一年不与劳动者订立书面劳动合同的,视为用人单位与劳动者已订立无固定期限劳动合同。
**第十五条 以完成一定工作任务为期限的劳动合同,是指用人单位与劳动者约定以某项工作的完成为合同期限的劳动合同。**
用人单位与劳动者协商一致,可以订立以完成一定工作任务为期限的劳动合同。
**第十六条 劳动合同由用人单位与劳动者协商一致,并经用人单位与劳动者在劳动合同文本上签字或者盖章生效。**
劳动合同文本由用人单位和劳动者各执一份。
**第十七条 劳动合同应当具备以下条款:**
(一)用人单位的名称、住所和法定代表人或者主要负责人;
(二)劳动者的姓名、住址和居民身份证或者其他有效身份证件号码;
(三)劳动合同期限;
(四)工作内容和工作地点;
(五)工作时间和休息休假;
(六)劳动报酬;
(七)社会保险;
(八)劳动保护、劳动条件和职业危害防护;
(九)法律、法规规定应当纳入劳动合同的其他事项。
劳动合同除前款规定的必备条款外,用人单位与劳动者可以约定试用期、培训、保守秘密、补充保险和福利待遇等其他事项。
**第十八条 劳动合同对劳动报酬和劳动条件等标准约定不明确,引发争议的,用人单位与劳动者可以重新协商;协商不成的,适用集体合同规定;没有集体合同或者集体合同未规定劳动报酬的,实行同工同酬;没有集体合同或者集体合同未规定劳动条件等标准的,适用国家有关规定。**
**第十九条 劳动合同期限三个月以上不满一年的,试用期不得超过一个月;劳动合同期限一年以上不满三年的,试用期不得超过二个月;三年以上固定期限和无固定期限的劳动合同,试用期不得超过六个月。**
同一用人单位与同一劳动者只能约定一次试用期。
以完成一定工作任务为期限的劳动合同或者劳动合同期限不满三个月的,不得约定试用期。
试用期包含在劳动合同期限内。劳动合同仅约定试用期的,试用期不成立,该期限为劳动合同期限。
**第二十条 劳动者在试用期的工资不得低于本单位相同岗位最低档工资或者劳动合同约定工资的百分之八十,并不得低于用人单位所在地的最低工资标准。**
**第二十一条 在试用期中,除劳动者有本法第三十九条和第四十条第一项、第二项规定的情形外,用人单位不得解除劳动合同。用人单位在试用期解除劳动合同的,应当向劳动者说明理由。**
**第二十二条 用人单位为劳动者提供专项培训费用,对其进行专业技术培训的,可以与该劳动者订立协议,约定服务期。**
劳动者违反服务期约定的,应当按照约定向用人单位支付违约金。违约金的数额不得超过用人单位提供的培训费用。用人单位要求劳动者支付的违约金不得超过服务期尚未履行部分所应分摊的培训费用。
用人单位与劳动者约定服务期的,不影响按照正常的工资调整机制提高劳动者在服务期期间的劳动报酬。
**第二十三条 用人单位与劳动者可以在劳动合同中约定保守用人单位的商业秘密和与知识产权相关的保密事项。**
对负有保密义务的劳动者,用人单位可以在劳动合同或者保密协议中与劳动者约定竞业限制条款,并约定在解除或者终止劳动合同后,在竞业限制期限内按月给予劳动者经济补偿。劳动者违反竞业限制约定的,应当按照约定向用人单位支付违约金。
**第二十四条 竞业限制的人员限于用人单位的高级管理人员、高级技术人员和其他负有保密义务的人员。竞业限制的范围、地域、期限由用人单位与劳动者约定,竞业限制的约定不得违反法律、法规的规定。**
在解除或者终止劳动合同后,前款规定的人员到与本单位生产或者经营同类产品、从事同类业务的有竞争关系的其他用人单位,或者自己开业生产或者经营同类产品、从事同类业务的竞业限制期限,不得超过二年。
**第二十五条 除本法第二十二条和第二十三条规定的情形外,用人单位不得与劳动者约定由劳动者承担违约金。**
**第二十六条 下列劳动合同无效或者部分无效:**
(一)以欺诈、胁迫的手段或者乘人之危,使对方在违背真实意思的情况下订立或者变更劳动合同的;
(二)用人单位免除自己的法定责任、排除劳动者权利的;
(三)违反法律、行政法规强制性规定的。
对劳动合同的无效或者部分无效有争议的,由劳动争议仲裁机构或者人民法院确认。
**第二十七条 劳动合同部分无效,不影响其他部分效力的,其他部分仍然有效。**
**第二十八条 劳动合同被确认无效,劳动者已付出劳动的,用人单位应当向劳动者支付劳动报酬。劳动报酬的数额,参照本单位相同或者相近岗位劳动者的劳动报酬确定。**
## 第三章 劳动合同的履行和变更
**第二十九条 用人单位与劳动者应当按照劳动合同的约定,全面履行各自的义务。**
**第三十条 用人单位应当按照劳动合同约定和国家规定,向劳动者及时足额支付劳动报酬。**
用人单位拖欠或者未足额支付劳动报酬的,劳动者可以依法向当地人民法院申请支付令,人民法院应当依法发出支付令。
**第三十一条 用人单位应当严格执行劳动定额标准,不得强迫或者变相强迫劳动者加班。用人单位安排加班的,应当按照国家有关规定向劳动者支付加班费。**
**第三十二条 劳动者拒绝用人单位管理人员违章指挥、强令冒险作业的,不视为违反劳动合同。**
劳动者对危害生命安全和身体健康的劳动条件,有权对用人单位提出批评、检举和控告。
**第三十三条 用人单位变更名称、法定代表人、主要负责人或者投资人等事项,不影响劳动合同的履行。**
**第三十四条 用人单位发生合并或者分立等情况,原劳动合同继续有效,劳动合同由承继其权利和义务的用人单位继续履行。**
**第三十五条 用人单位与劳动者协商一致,可以变更劳动合同约定的内容。变更劳动合同,应当采用书面形式。**
变更后的劳动合同文本由用人单位和劳动者各执一份。
## 第四章 劳动合同的解除和终止
**第三十六条 用人单位与劳动者协商一致,可以解除劳动合同。**
**第三十七条 劳动者提前三十日以书面形式通知用人单位,可以解除劳动合同。劳动者在试用期内提前三日通知用人单位,可以解除劳动合同。**
**第三十八条 用人单位有下列情形之一的,劳动者可以解除劳动合同:**
(一)未按照劳动合同约定提供劳动保护或者劳动条件的;
(二)未及时足额支付劳动报酬的;
(三)未依法为劳动者缴纳社会保险费的;
(四)用人单位的规章制度违反法律、法规的规定,损害劳动者权益的;
(五)因本法第二十六条第一款规定的情形致使劳动合同无效的;
(六)法律、行政法规规定劳动者可以解除劳动合同的其他情形。
用人单位以暴力、威胁或者非法限制人身自由的手段强迫劳动者劳动的,或者用人单位违章指挥、强令冒险作业危及劳动者人身安全的,劳动者可以立即解除劳动合同,不需事先告知用人单位。
**第三十九条 劳动者有下列情形之一的,用人单位可以解除劳动合同:**
(一)在试用期间被证明不符合录用条件的;
(二)严重违反用人单位的规章制度的;
(三)严重失职,营私舞弊,给用人单位造成重大损害的;
(四)劳动者同时与其他用人单位建立劳动关系,对完成本单位的工作任务造成严重影响,或者经用人单位提出,拒不改正的;
(五)因本法第二十六条第一款第一项规定的情形致使劳动合同无效的;
(六)被依法追究刑事责任的。
**第四十条 有下列情形之一的,用人单位提前三十日以书面形式通知劳动者本人或者额外支付劳动者一个月工资后,可以解除劳动合同:**
(一)劳动者患病或者非因工负伤,在规定的医疗期满后不能从事原工作,也不能从事由用人单位另行安排的工作的;
(二)劳动者不能胜任工作,经过培训或者调整工作岗位,仍不能胜任工作的;
(三)劳动合同订立时所依据的客观情况发生重大变化,致使劳动合同无法履行,经用人单位与劳动者协商,未能就变更劳动合同内容达成协议的。
**第四十一条 有下列情形之一,需要裁减人员二十人以上或者裁减不足二十人但占企业职工总数百分之十以上的,用人单位提前三十日向工会或者全体职工说明情况,听取工会或者职工的意见后,裁减人员方案经向劳动行政部门报告,可以裁减人员:**
(一)依照企业破产法规定进行重整的;
(二)生产经营发生严重困难的;
(三)企业转产、重大技术革新或者经营方式调整,经变更劳动合同后,仍需裁减人员的;
(四)其他因劳动合同订立时所依据的客观经济情况发生重大变化,致使劳动合同无法履行的。
裁减人员时,应当优先留用下列人员:
(一)与本单位订立较长期限的固定期限劳动合同的;
(二)与本单位订立无固定期限劳动合同的;
(三)家庭无其他就业人员,有需要扶养的老人或者未成年人的。
用人单位依照本条第一款规定裁减人员,在六个月内重新招用人员的,应当通知被裁减的人员,并在同等条件下优先招用被裁减的人员。
**第四十二条 劳动者有下列情形之一的,用人单位不得依照本法第四十条、第四十一条的规定解除劳动合同:**
(一)从事接触职业病危害作业的劳动者未进行离岗前职业健康检查,或者疑似职业病病人在诊断或者医学观察期间的;
(二)在本单位患职业病或者因工负伤并被确认丧失或者部分丧失劳动能力的;
(三)患病或者非因工负伤,在规定的医疗期内的;
(四)女职工在孕期、产期、哺乳期的;
(五)在本单位连续工作满十五年,且距法定退休年龄不足五年的;
(六)法律、行政法规规定的其他情形。
**第四十三条 用人单位单方解除劳动合同,应当事先将理由通知工会。用人单位违反法律、行政法规规定或者劳动合同约定的,工会有权要求用人单位纠正。用人单位应当研究工会的意见,并将处理结果书面通知工会。**
**第四十四条 有下列情形之一的,劳动合同终止:**
(一)劳动合同期满的;
(二)劳动者开始依法享受基本养老保险待遇的;
(三)劳动者死亡,或者被人民法院宣告死亡或者宣告失踪的;
(四)用人单位被依法宣告破产的;
(五)用人单位被吊销营业执照、责令关闭、撤销或者用人单位决定提前解散的;
(六)法律、行政法规规定的其他情形。
**第四十五条 劳动合同期满,有本法第四十二条规定情形之一的,劳动合同应当续延至相应的情形消失时终止。但是,本法第四十二条第二项规定丧失或者部分丧失劳动能力劳动者的劳动合同的终止,按照国家有关工伤保险的规定执行。**
**第四十六条 有下列情形之一的,用人单位应当向劳动者支付经济补偿:**
(一)劳动者依照本法第三十八条规定解除劳动合同的;
(二)用人单位依照本法第三十六条规定向劳动者提出解除劳动合同并与劳动者协商一致解除劳动合同的;
(三)用人单位依照本法第四十条规定解除劳动合同的;
(四)用人单位依照本法第四十一条第一款规定解除劳动合同的;
(五)除用人单位维持或者提高劳动合同约定条件续订劳动合同,劳动者不同意续订的情形外,依照本法第四十四条第一项规定终止固定期限劳动合同的;
(六)依照本法第四十四条第四项、第五项规定终止劳动合同的;
(七)法律、行政法规规定的其他情形。
**第四十七条 经济补偿按劳动者在本单位工作的年限,每满一年支付一个月工资的标准向劳动者支付。六个月以上不满一年的,按一年计算;不满六个月的,向劳动者支付半个月工资的经济补偿。**
劳动者月工资高于用人单位所在直辖市、设区的市级人民政府公布的本地区上年度职工月平均工资三倍的,向其支付经济补偿的标准按职工月平均工资三倍的数额支付,向其支付经济补偿的年限最高不超过十二年。
本条所称月工资是指劳动者在劳动合同解除或者终止前十二个月的平均工资。
**第四十八条 用人单位违反本法规定解除或者终止劳动合同,劳动者要求继续履行劳动合同的,用人单位应当继续履行;劳动者不要求继续履行劳动合同或者劳动合同已经不能继续履行的,用人单位应当依照本法第八十七条规定支付赔偿金。**
**第四十九条 国家采取措施,建立健全劳动者社会保险关系跨地区转移接续制度。**
**第五十条 用人单位应当在解除或者终止劳动合同时出具解除或者终止劳动合同的证明,并在十五日内为劳动者办理档案和社会保险关系转移手续。**
劳动者应当按照双方约定,办理工作交接。用人单位依照本法有关规定应当向劳动者支付经济补偿的,在办结工作交接时支付。
用人单位对已经解除或者终止的劳动合同的文本,至少保存二年备查。
## 第五章 特别规定
第一节 集体合同
**第五十一条 企业职工一方与用人单位通过平等协商,可以就劳动报酬、工作时间、休息休假、劳动安全卫生、保险福利等事项订立集体合同。集体合同草案应当提交职工代表大会或者全体职工讨论通过。**
集体合同由工会代表企业职工一方与用人单位订立;尚未建立工会的用人单位,由上级工会指导劳动者推举的代表与用人单位订立。
**第五十二条 企业职工一方与用人单位可以订立劳动安全卫生、女职工权益保护、工资调整机制等专项集体合同。**
**第五十三条 在县级以下区域内,建筑业、采矿业、餐饮服务业等行业可以由工会与企业方面代表订立行业性集体合同,或者订立区域性集体合同。**
**第五十四条 集体合同订立后,应当报送劳动行政部门;劳动行政部门自收到集体合同文本之日起十五日内未提出异议的,集体合同即行生效。**
依法订立的集体合同对用人单位和劳动者具有约束力。行业性、区域性集体合同对当地本行业、本区域的用人单位和劳动者具有约束力。
**第五十五条 集体合同中劳动报酬和劳动条件等标准不得低于当地人民政府规定的最低标准;用人单位与劳动者订立的劳动合同中劳动报酬和劳动条件等标准不得低于集体合同规定的标准。**
**第五十六条 用人单位违反集体合同,侵犯职工劳动权益的,工会可以依法要求用人单位承担责任;因履行集体合同发生争议,经协商解决不成的,工会可以依法申请仲裁、提起诉讼。**
第二节 劳务派遣
**第五十七条 经营劳务派遣业务应当具备下列条件:**
(一)注册资本不得少于人民币二百万元;
(二)有与开展业务相适应的固定的经营场所和设施;
(三)有符合法律、行政法规规定的劳务派遣管理制度;
(四)法律、行政法规规定的其他条件。
经营劳务派遣业务,应当向劳动行政部门依法申请行政许可;经许可的,依法办理相应的公司登记。未经许可,任何单位和个人不得经营劳务派遣业务。
**第五十八条 劳务派遣单位是本法所称用人单位,应当履行用人单位对劳动者的义务。劳务派遣单位与被派遣劳动者订立的劳动合同,除应当载明本法第十七条规定的事项外,还应当载明被派遣劳动者的用工单位以及派遣期限、工作岗位等情况。**
劳务派遣单位应当与被派遣劳动者订立二年以上的固定期限劳动合同,按月支付劳动报酬;被派遣劳动者在无工作期间,劳务派遣单位应当按照所在地人民政府规定的最低工资标准,向其按月支付报酬。
**第五十九条 劳务派遣单位派遣劳动者应当与接受以劳务派遣形式用工的单位(以下称用工单位)订立劳务派遣协议。劳务派遣协议应当约定派遣岗位和人员数量、派遣期限、劳动报酬和社会保险费的数额与支付方式以及违反协议的责任。**
用工单位应当根据工作岗位的实际需要与劳务派遣单位确定派遣期限,不得将连续用工期限分割订立数个短期劳务派遣协议。
**第六十条 劳务派遣单位应当将劳务派遣协议的内容告知被派遣劳动者。**
劳务派遣单位不得克扣用工单位按照劳务派遣协议支付给被派遣劳动者的劳动报酬。
劳务派遣单位和用工单位不得向被派遣劳动者收取费用。
**第六十一条 劳务派遣单位跨地区派遣劳动者的,被派遣劳动者享有的劳动报酬和劳动条件,按照用工单位所在地的标准执行。**
**第六十二条 用工单位应当履行下列义务:**
(一)执行国家劳动标准,提供相应的劳动条件和劳动保护;
(二)告知被派遣劳动者的工作要求和劳动报酬;
(三)支付加班费、绩效奖金,提供与工作岗位相关的福利待遇;
(四)对在岗被派遣劳动者进行工作岗位所必需的培训;
(五)连续用工的,实行正常的工资调整机制。
用工单位不得将被派遣劳动者再派遣到其他用人单位。
**第六十三条 被派遣劳动者享有与用工单位的劳动者同工同酬的权利。用工单位应当按照同工同酬原则,对被派遣劳动者与本单位同类岗位的劳动者实行相同的劳动报酬分配办法。用工单位无同类岗位劳动者的,参照用工单位所在地相同或者相近岗位劳动者的劳动报酬确定。**
劳务派遣单位与被派遣劳动者订立的劳动合同和与用工单位订立的劳务派遣协议,载明或者约定的向被派遣劳动者支付的劳动报酬应当符合前款规定。
**第六十四条 被派遣劳动者有权在劳务派遣单位或者用工单位依法参加或者组织工会,维护自身的合法权益。**
**第六十五条 被派遣劳动者可以依照本法第三十六条、第三十八条的规定与劳务派遣单位解除劳动合同。**
被派遣劳动者有本法第三十九条和第四十条第一项、第二项规定情形的,用工单位可以将劳动者退回劳务派遣单位,劳务派遣单位依照本法有关规定,可以与劳动者解除劳动合同。
**第六十六条 劳动合同用工是我国的企业基本用工形式。劳务派遣用工是补充形式,只能在临时性、辅助性或者替代性的工作岗位上实施。**
前款规定的临时性工作岗位是指存续时间不超过六个月的岗位;辅助性工作岗位是指为主营业务岗位提供服务的非主营业务岗位;替代性工作岗位是指用工单位的劳动者因脱产学习、休假等原因无法工作的一定期间内,可以由其他劳动者替代工作的岗位。
用工单位应当严格控制劳务派遣用工数量,不得超过其用工总量的一定比例,具体比例由国务院劳动行政部门规定。
**第六十七条 用人单位不得设立劳务派遣单位向本单位或者所属单位派遣劳动者。**
第三节 非全日制用工
**第六十八条 非全日制用工,是指以小时计酬为主,劳动者在同一用人单位一般平均每日工作时间不超过四小时,每周工作时间累计不超过二十四小时的用工形式。**
**第六十九条 非全日制用工双方当事人可以订立口头协议。**
从事非全日制用工的劳动者可以与一个或者一个以上用人单位订立劳动合同;但是,后订立的劳动合同不得影响先订立的劳动合同的履行。
**第七十条 非全日制用工双方当事人不得约定试用期。**
**第七十一条 非全日制用工双方当事人任何一方都可以随时通知对方终止用工。终止用工,用人单位不向劳动者支付经济补偿。**
**第七十二条 非全日制用工小时计酬标准不得低于用人单位所在地人民政府规定的最低小时工资标准。**
非全日制用工劳动报酬结算支付周期最长不得超过十五日。
## 第六章 监督检查
**第七十三条 国务院劳动行政部门负责全国劳动合同制度实施的监督管理。**
县级以上地方人民政府劳动行政部门负责本行政区域内劳动合同制度实施的监督管理。
县级以上各级人民政府劳动行政部门在劳动合同制度实施的监督管理工作中,应当听取工会、企业方面代表以及有关行业主管部门的意见。
**第七十四条 县级以上地方人民政府劳动行政部门依法对下列实施劳动合同制度的情况进行监督检查:**
(一)用人单位制定直接涉及劳动者切身利益的规章制度及其执行的情况;
(二)用人单位与劳动者订立和解除劳动合同的情况;
(三)劳务派遣单位和用工单位遵守劳务派遣有关规定的情况;
(四)用人单位遵守国家关于劳动者工作时间和休息休假规定的情况;
(五)用人单位支付劳动合同约定的劳动报酬和执行最低工资标准的情况;
(六)用人单位参加各项社会保险和缴纳社会保险费的情况;
(七)法律、法规规定的其他劳动监察事项。
**第七十五条 县级以上地方人民政府劳动行政部门实施监督检查时,有权查阅与劳动合同、集体合同有关的材料,有权对劳动场所进行实地检查,用人单位和劳动者都应当如实提供有关情况和材料。**
劳动行政部门的工作人员进行监督检查,应当出示证件,依法行使职权,文明执法。
**第七十六条 县级以上人民政府建设、卫生、安全生产监督管理等有关主管部门在各自职责范围内,对用人单位执行劳动合同制度的情况进行监督管理。**
**第七十七条 劳动者合法权益受到侵害的,有权要求有关部门依法处理,或者依法申请仲裁、提起诉讼。**
**第七十八条 工会依法维护劳动者的合法权益,对用人单位履行劳动合同、集体合同的情况进行监督。用人单位违反劳动法律、法规和劳动合同、集体合同的,工会有权提出意见或者要求纠正;劳动者申请仲裁、提起诉讼的,工会依法给予支持和帮助。**
**第七十九条 任何组织或者个人对违反本法的行为都有权举报,县级以上人民政府劳动行政部门应当及时核实、处理,并对举报有功人员给予奖励。**
## 第七章 法律责任
**第八十条 用人单位直接涉及劳动者切身利益的规章制度违反法律、法规规定的,由劳动行政部门责令改正,给予警告;给劳动者造成损害的,应当承担赔偿责任。**
**第八十一条 用人单位提供的劳动合同文本未载明本法规定的劳动合同必备条款或者用人单位未将劳动合同文本交付劳动者的,由劳动行政部门责令改正;给劳动者造成损害的,应当承担赔偿责任。**
**第八十二条 用人单位自用工之日起超过一个月不满一年未与劳动者订立书面劳动合同的,应当向劳动者每月支付二倍的工资。**
用人单位违反本法规定不与劳动者订立无固定期限劳动合同的,自应当订立无固定期限劳动合同之日起向劳动者每月支付二倍的工资。
**第八十三条 用人单位违反本法规定与劳动者约定试用期的,由劳动行政部门责令改正;违法约定的试用期已经履行的,由用人单位以劳动者试用期满月工资为标准,按已经履行的超过法定试用期的期间向劳动者支付赔偿金。**
**第八十四条 用人单位违反本法规定,扣押劳动者居民身份证等证件的,由劳动行政部门责令限期退还劳动者本人,并依照有关法律规定给予处罚。**
用人单位违反本法规定,以担保或者其他名义向劳动者收取财物的,由劳动行政部门责令限期退还劳动者本人,并以每人五百元以上二千元以下的标准处以罚款;给劳动者造成损害的,应当承担赔偿责任。
劳动者依法解除或者终止劳动合同,用人单位扣押劳动者档案或者其他物品的,依照前款规定处罚。
**第八十五条 用人单位有下列情形之一的,由劳动行政部门责令限期支付劳动报酬、加班费或者经济补偿;劳动报酬低于当地最低工资标准的,应当支付其差额部分;逾期不支付的,责令用人单位按应付金额百分之五十以上百分之一百以下的标准向劳动者加付赔偿金:**
(一)未按照劳动合同的约定或者国家规定及时足额支付劳动者劳动报酬的;
(二)低于当地最低工资标准支付劳动者工资的;
(三)安排加班不支付加班费的;
(四)解除或者终止劳动合同,未依照本法规定向劳动者支付经济补偿的。
**第八十六条 劳动合同依照本法第二十六条规定被确认无效,给对方造成损害的,有过错的一方应当承担赔偿责任。**
**第八十七条 用人单位违反本法规定解除或者终止劳动合同的,应当依照本法第四十七条规定的经济补偿标准的二倍向劳动者支付赔偿金。**
**第八十八条 用人单位有下列情形之一的,依法给予行政处罚;构成犯罪的,依法追究刑事责任;给劳动者造成损害的,应当承担赔偿责任:**
(一)以暴力、威胁或者非法限制人身自由的手段强迫劳动的;
(二)违章指挥或者强令冒险作业危及劳动者人身安全的;
(三)侮辱、体罚、殴打、非法搜查或者拘禁劳动者的;
(四)劳动条件恶劣、环境污染严重,给劳动者身心健康造成严重损害的。
**第八十九条 用人单位违反本法规定未向劳动者出具解除或者终止劳动合同的书面证明,由劳动行政部门责令改正;给劳动者造成损害的,应当承担赔偿责任。**
**第九十条 劳动者违反本法规定解除劳动合同,或者违反劳动合同中约定的保密义务或者竞业限制,给用人单位造成损失的,应当承担赔偿责任。**
**第九十一条 用人单位招用与其他用人单位尚未解除或者终止劳动合同的劳动者,给其他用人单位造成损失的,应当承担连带赔偿责任。**
**第九十二条 违反本法规定,未经许可,擅自经营劳务派遣业务的,由劳动行政部门责令停止违法行为,没收违法所得,并处违法所得一倍以上五倍以下的罚款;没有违法所得的,可以处五万元以下的罚款。**
劳务派遣单位、用工单位违反本法有关劳务派遣规定的,由劳动行政部门责令限期改正;逾期不改正的,以每人五千元以上一万元以下的标准处以罚款,对劳务派遣单位,吊销其劳务派遣业务经营许可证。用工单位给被派遣劳动者造成损害的,劳务派遣单位与用工单位承担连带赔偿责任。
**第九十三条 对不具备合法经营资格的用人单位的违法犯罪行为,依法追究法律责任;劳动者已经付出劳动的,该单位或者其出资人应当依照本法有关规定向劳动者支付劳动报酬、经济补偿、赔偿金;给劳动者造成损害的,应当承担赔偿责任。**
**第九十四条 个人承包经营违反本法规定招用劳动者,给劳动者造成损害的,发包的组织与个人承包经营者承担连带赔偿责任。**
**第九十五条 劳动行政部门和其他有关主管部门及其工作人员玩忽职守、不履行法定职责,或者违法行使职权,给劳动者或者用人单位造成损害的,应当承担赔偿责任;对直接负责的主管人员和其他直接责任人员,依法给予行政处分;构成犯罪的,依法追究刑事责任。**
## 第八章 附 则
**第九十六条 事业单位与实行聘用制的工作人员订立、履行、变更、解除或者终止劳动合同,法律、行政法规或者国务院另有规定的,依照其规定;未作规定的,依照本法有关规定执行。**
**第九十七条 本法施行前已依法订立且在本法施行之日存续的劳动合同,继续履行;本法第十四条第二款第三项规定连续订立固定期限劳动合同的次数,自本法施行后续订固定期限劳动合同时开始计算。**
本法施行前已建立劳动关系,尚未订立书面劳动合同的,应当自本法施行之日起一个月内订立。
本法施行之日存续的劳动合同在本法施行后解除或者终止,依照本法第四十六条规定应当支付经济补偿的,经济补偿年限自本法施行之日起计算;本法施行前按照当时有关规定,用人单位应当向劳动者支付经济补偿的,按照当时有关规定执行。
**第九十八条 本法自2008年1月1日起施行。**
FILE:references/complete/劳动合同法实施条例.md
# 中华人民共和国劳动合同法实施条例(完整版)
中华人民共和国劳动合同法实施条例
(2008年9月3日国务院第25次常务会议通过 2008年9月18日中华人民共和国国务院令第535号公布 自公布之日起施行)
## 第一章 总则
**第一条 为了贯彻实施《中华人民共和国劳动合同法》(以下简称劳动合同法),制定本条例。**
**第二条 各级人民政府和县级以上人民政府劳动行政等有关部门以及工会等组织,应当采取措施,推动劳动合同法的贯彻实施,促进劳动关系的和谐。**
**第三条 依法成立的会计师事务所、律师事务所等合伙组织和基金会,属于劳动合同法规定的用人单位。**
## 第二章 劳动合同的订立
**第四条 劳动合同法规定的用人单位设立的分支机构,依法取得营业执照或者登记证书的,可以作为用人单位与劳动者订立劳动合同;未依法取得营业执照或者登记证书的,受用人单位委托可以与劳动者订立劳动合同。**
**第五条 自用工之日起一个月内,经用人单位书面通知后,劳动者不与用人单位订立书面劳动合同的,用人单位应当书面通知劳动者终止劳动关系,无需向劳动者支付经济补偿,但是应当依法向劳动者支付其实际工作时间的劳动报酬。**
**第六条 用人单位自用工之日起超过一个月不满一年未与劳动者订立书面劳动合同的,应当依照劳动合同法第八十二条的规定向劳动者每月支付两倍的工资,并与劳动者补订书面劳动合同;劳动者不与用人单位订立书面劳动合同的,用人单位应当书面通知劳动者终止劳动关系,并依照劳动合同法第四十七条的规定支付经济补偿。**
前款规定的用人单位向劳动者每月支付两倍工资的起算时间为用工之日起满一个月的次日,截止时间为补订书面劳动合同的前一日。
**第七条 用人单位自用工之日起满一年未与劳动者订立书面劳动合同的,自用工之日起满一个月的次日至满一年的前一日应当依照劳动合同法第八十二条的规定向劳动者每月支付两倍的工资,并视为自用工之日起满一年的当日已经与劳动者订立无固定期限劳动合同,应当立即与劳动者补订书面劳动合同。**
**第八条 劳动合同法第七条规定的职工名册,应当包括劳动者姓名、性别、公民身份号码、户籍地址及现住址、联系方式、用工形式、用工起始时间、劳动合同期限等内容。**
**第九条 劳动合同法第十四条第二款规定的连续工作满10年的起始时间,应当自用人单位用工之日起计算,包括劳动合同法施行前的工作年限。**
**第十条 劳动者非因本人原因从原用人单位被安排到新用人单位工作的,劳动者在原用人单位的工作年限合并计算为新用人单位的工作年限。原用人单位已经向劳动者支付经济补偿的,新用人单位在依法解除、终止劳动合同计算支付经济补偿的工作年限时,不再计算劳动者在原用人单位的工作年限。**
**第十一条 除劳动者与用人单位协商一致的情形外,劳动者依照劳动合同法第十四条第二款的规定,提出订立无固定期限劳动合同的,用人单位应当与其订立无固定期限劳动合同。对劳动合同的内容,双方应当按照合法、公平、平等自愿、协商一致、诚实信用的原则协商确定;对协商不一致的内容,依照劳动合同法第十八条的规定执行。**
**第十二条 地方各级人民政府及县级以上地方人民政府有关部门为安置就业困难人员提供的给予岗位补贴和社会保险补贴的公益性岗位,其劳动合同不适用劳动合同法有关无固定期限劳动合同的规定以及支付经济补偿的规定。**
**第十三条 用人单位与劳动者不得在劳动合同法第四十四条规定的劳动合同终止情形之外约定其他的劳动合同终止条件。**
**第十四条 劳动合同履行地与用人单位注册地不一致的,有关劳动者的最低工资标准、劳动保护、劳动条件、职业危害防护和本地区上年度职工月平均工资标准等事项,按照劳动合同履行地的有关规定执行;用人单位注册地的有关标准高于劳动合同履行地的有关标准,且用人单位与劳动者约定按照用人单位注册地的有关规定执行的,从其约定。**
**第十五条 劳动者在试用期的工资不得低于本单位相同岗位最低档工资的80%或者不得低于劳动合同约定工资的80%,并不得低于用人单位所在地的最低工资标准。**
**第十六条 劳动合同法第二十二条第二款规定的培训费用,包括用人单位为了对劳动者进行专业技术培训而支付的有凭证的培训费用、培训期间的差旅费用以及因培训产生的用于该劳动者的其他直接费用。**
**第十七条 劳动合同期满,但是用人单位与劳动者依照劳动合同法第二十二条的规定约定的服务期尚未到期的,劳动合同应当续延至服务期满;双方另有约定的,从其约定。**
## 第三章 劳动合同的解除和终止
**第十八条 有下列情形之一的,依照劳动合同法规定的条件、程序,劳动者可以与用人单位解除固定期限劳动合同、无固定期限劳动合同或者以完成一定工作任务为期限的劳动合同:**
(一)劳动者与用人单位协商一致的;
(二)劳动者提前30日以书面形式通知用人单位的;
(三)劳动者在试用期内提前3日通知用人单位的;
(四)用人单位未按照劳动合同约定提供劳动保护或者劳动条件的;
(五)用人单位未及时足额支付劳动报酬的;
(六)用人单位未依法为劳动者缴纳社会保险费的;
(七)用人单位的规章制度违反法律、法规的规定,损害劳动者权益的;
(八)用人单位以欺诈、胁迫的手段或者乘人之危,使劳动者在违背真实意思的情况下订立或者变更劳动合同的;
(九)用人单位在劳动合同中免除自己的法定责任、排除劳动者权利的;
(十)用人单位违反法律、行政法规强制性规定的;
(十一)用人单位以暴力、威胁或者非法限制人身自由的手段强迫劳动者劳动的;
(十二)用人单位违章指挥、强令冒险作业危及劳动者人身安全的;
(十三)法律、行政法规规定劳动者可以解除劳动合同的其他情形。
**第十九条 有下列情形之一的,依照劳动合同法规定的条件、程序,用人单位可以与劳动者解除固定期限劳动合同、无固定期限劳动合同或者以完成一定工作任务为期限的劳动合同:**
(一)用人单位与劳动者协商一致的;
(二)劳动者在试用期间被证明不符合录用条件的;
(三)劳动者严重违反用人单位的规章制度的;
(四)劳动者严重失职,营私舞弊,给用人单位造成重大损害的;
(五)劳动者同时与其他用人单位建立劳动关系,对完成本单位的工作任务造成严重影响,或者经用人单位提出,拒不改正的;
(六)劳动者以欺诈、胁迫的手段或者乘人之危,使用人单位在违背真实意思的情况下订立或者变更劳动合同的;
(七)劳动者被依法追究刑事责任的;
(八)劳动者患病或者非因工负伤,在规定的医疗期满后不能从事原工作,也不能从事由用人单位另行安排的工作的;
(九)劳动者不能胜任工作,经过培训或者调整工作岗位,仍不能胜任工作的;
(十)劳动合同订立时所依据的客观情况发生重大变化,致使劳动合同无法履行,经用人单位与劳动者协商,未能就变更劳动合同内容达成协议的;
(十一)用人单位依照企业破产法规定进行重整的;
(十二)用人单位生产经营发生严重困难的;
(十三)企业转产、重大技术革新或者经营方式调整,经变更劳动合同后,仍需裁减人员的;
(十四)其他因劳动合同订立时所依据的客观经济情况发生重大变化,致使劳动合同无法履行的。
**第二十条 用人单位依照劳动合同法第四十条的规定,选择额外支付劳动者一个月工资解除劳动合同的,其额外支付的工资应当按照该劳动者上一个月的工资标准确定。**
**第二十一条 劳动者达到法定退休年龄的,劳动合同终止。**
**第二十二条 以完成一定工作任务为期限的劳动合同因任务完成而终止的,用人单位应当依照劳动合同法第四十七条的规定向劳动者支付经济补偿。**
**第二十三条 用人单位依法终止工伤职工的劳动合同的,除依照劳动合同法第四十七条的规定支付经济补偿外,还应当依照国家有关工伤保险的规定支付一次性工伤医疗补助金和伤残就业补助金。**
**第二十四条 用人单位出具的解除、终止劳动合同的证明,应当写明劳动合同期限、解除或者终止劳动合同的日期、工作岗位、在本单位的工作年限。**
**第二十五条 用人单位违反劳动合同法的规定解除或者终止劳动合同,依照劳动合同法第八十七条的规定支付了赔偿金的,不再支付经济补偿。赔偿金的计算年限自用工之日起计算。**
**第二十六条 用人单位与劳动者约定了服务期,劳动者依照劳动合同法第三十八条的规定解除劳动合同的,不属于违反服务期的约定,用人单位不得要求劳动者支付违约金。**
有下列情形之一,用人单位与劳动者解除约定服务期的劳动合同的,劳动者应当按照劳动合同的约定向用人单位支付违约金:
(一)劳动者严重违反用人单位的规章制度的;
(二)劳动者严重失职,营私舞弊,给用人单位造成重大损害的;
(三)劳动者同时与其他用人单位建立劳动关系,对完成本单位的工作任务造成严重影响,或者经用人单位提出,拒不改正的;
(四)劳动者以欺诈、胁迫的手段或者乘人之危,使用人单位在违背真实意思的情况下订立或者变更劳动合同的;
(五)劳动者被依法追究刑事责任的。
**第二十七条 劳动合同法第四十七条规定的经济补偿的月工资按照劳动者应得工资计算,包括计时工资或者计件工资以及奖金、津贴和补贴等货币性收入。劳动者在劳动合同解除或者终止前12个月的平均工资低于当地最低工资标准的,按照当地最低工资标准计算。劳动者工作不满12个月的,按照实际工作的月数计算平均工资。**
## 第四章 劳务派遣特别规定
**第二十八条 用人单位或者其所属单位出资或者合伙设立的劳务派遣单位,向本单位或者所属单位派遣劳动者的,属于劳动合同法第六十七条规定的不得设立的劳务派遣单位。**
**第二十九条 用工单位应当履行劳动合同法第六十二条规定的义务,维护被派遣劳动者的合法权益。**
**第三十条 劳务派遣单位不得以非全日制用工形式招用被派遣劳动者。**
**第三十一条 劳务派遣单位或者被派遣劳动者依法解除、终止劳动合同的经济补偿,依照劳动合同法第四十六条、第四十七条的规定执行。**
**第三十二条 劳务派遣单位违法解除或者终止被派遣劳动者的劳动合同的,依照劳动合同法第四十八条的规定执行。**
## 第五章 法律责任
**第三十三条 用人单位违反劳动合同法有关建立职工名册规定的,由劳动行政部门责令限期改正;逾期不改正的,由劳动行政部门处2000元以上2万元以下的罚款。**
**第三十四条 用人单位依照劳动合同法的规定应当向劳动者每月支付两倍的工资或者应当向劳动者支付赔偿金而未支付的,劳动行政部门应当责令用人单位支付。**
**第三十五条 用工单位违反劳动合同法和本条例有关劳务派遣规定的,由劳动行政部门和其他有关主管部门责令改正;情节严重的,以每位被派遣劳动者1000元以上5000元以下的标准处以罚款;给被派遣劳动者造成损害的,劳务派遣单位和用工单位承担连带赔偿责任。**
## 第六章 附则
**第三十六条 对违反劳动合同法和本条例的行为的投诉、举报,县级以上地方人民政府劳动行政部门依照《劳动保障监察条例》的规定处理。**
**第三十七条 劳动者与用人单位因订立、履行、变更、解除或者终止劳动合同发生争议的,依照《中华人民共和国劳动争议调解仲裁法》的规定处理。**
**第三十八条 本条例自公布之日起施行。**
FILE:references/complete/司法解释(一).md
# 最高人民法院关于审理劳动争议案件适用法律问题的解释(一)(完整版)
最高人民法院
关于审理劳动争议案件适用法律问题的解释(一)
法释〔2020〕26号
(2020年12月25日最高人民法院审判委员会
第1825次会议通过,自2021年1月1日起施行)
为正确审理劳动争议案件,根据《中华人民共和国民法典》《中华人民共和国劳动法》《中华人民共和国劳动合同法》《中华人民共和国劳动争议调解仲裁法》《中华人民共和国民事诉讼法》等相关法律规定,结合审判实践,制定本解释。
**第一条 劳动者与用人单位之间发生的下列纠纷,属于劳动争议,当事人不服劳动争议仲裁机构作出的裁决,依法提起诉讼的,人民法院应予受理:**
(一)劳动者与用人单位在履行劳动合同过程中发生的纠纷;
(二)劳动者与用人单位之间没有订立书面劳动合同,但已形成劳动关系后发生的纠纷;
(三)劳动者与用人单位因劳动关系是否已经解除或者终止,以及应否支付解除或者终止劳动关系经济补偿金发生的纠纷;
(四)劳动者与用人单位解除或者终止劳动关系后,请求用人单位返还其收取的劳动合同定金、保证金、抵押金、抵押物发生的纠纷,或者办理劳动者的人事档案、社会保险关系等移转手续发生的纠纷;
(五)劳动者以用人单位未为其办理社会保险手续,且社会保险经办机构不能补办导致其无法享受社会保险待遇为由,要求用人单位赔偿损失发生的纠纷;
(六)劳动者退休后,与尚未参加社会保险统筹的原用人单位因追索养老金、医疗费、工伤保险待遇和其他社会保险待遇而发生的纠纷;
(七)劳动者因为工伤、职业病,请求用人单位依法给予工伤保险待遇发生的纠纷;
(八)劳动者依据劳动合同法第八十五条规定,要求用人单位支付加付赔偿金发生的纠纷;
(九)因企业自主进行改制发生的纠纷。
**第二条 下列纠纷不属于劳动争议:**
(一)劳动者请求社会保险经办机构发放社会保险金的纠纷;
(二)劳动者与用人单位因住房制度改革产生的公有住房转让纠纷;
(三)劳动者对劳动能力鉴定委员会的伤残等级鉴定结论或者对职业病诊断鉴定委员会的职业病诊断鉴定结论的异议纠纷;
(四)家庭或者个人与家政服务人员之间的纠纷;
(五)个体工匠与帮工、学徒之间的纠纷;
(六)农村承包经营户与受雇人之间的纠纷。
**第三条 劳动争议案件由用人单位所在地或者劳动合同履行地的基层人民法院管辖。**
劳动合同履行地不明确的,由用人单位所在地的基层人民法院管辖。
法律另有规定的,依照其规定。
**第四条 劳动者与用人单位均不服劳动争议仲裁机构的同一裁决,向同一人民法院起诉的,人民法院应当并案审理,双方当事人互为原告和被告,对双方的诉讼请求,人民法院应当一并作出裁决。在诉讼过程中,一方当事人撤诉的,人民法院应当根据另一方当事人的诉讼请求继续审理。双方当事人就同一仲裁裁决分别向有管辖权的人民法院起诉的,后受理的人民法院应当将案件移送给先受理的人民法院。**
**第五条 劳动争议仲裁机构以无管辖权为由对劳动争议案件不予受理,当事人提起诉讼的,人民法院按照以下情形分别处理:**
(一)经审查认为该劳动争议仲裁机构对案件确无管辖权的,应当告知当事人向有管辖权的劳动争议仲裁机构申请仲裁;
(二)经审查认为该劳动争议仲裁机构有管辖权的,应当告知当事人申请仲裁,并将审查意见书面通知该劳动争议仲裁机构;劳动争议仲裁机构仍不受理,当事人就该劳动争议事项提起诉讼的,人民法院应予受理。
**第六条 劳动争议仲裁机构以当事人申请仲裁的事项不属于劳动争议为由,作出不予受理的书面裁决、决定或者通知,当事人不服依法提起诉讼的,人民法院应当分别情况予以处理:**
(一)属于劳动争议案件的,应当受理;
(二)虽不属于劳动争议案件,但属于人民法院主管的其他案件,应当依法受理。
**第七条 劳动争议仲裁机构以申请仲裁的主体不适格为由,作出不予受理的书面裁决、决定或者通知,当事人不服依法提起诉讼,经审查确属主体不适格的,人民法院不予受理;已经受理的,裁定驳回起诉。**
**第八条 劳动争议仲裁机构为纠正原仲裁裁决错误重新作出裁决,当事人不服依法提起诉讼的,人民法院应当受理。**
**第九条 劳动争议仲裁机构仲裁的事项不属于人民法院受理的案件范围,当事人不服依法提起诉讼的,人民法院不予受理;已经受理的,裁定驳回起诉。**
**第十条 当事人不服劳动争议仲裁机构作出的预先支付劳动者劳动报酬、工伤医疗费、经济补偿或者赔偿金的裁决,依法提起诉讼的,人民法院不予受理。**
用人单位不履行上述裁决中的给付义务,劳动者依法申请强制执行的,人民法院应予受理。
**第十一条 劳动争议仲裁机构作出的调解书已经发生法律效力,一方当事人反悔提起诉讼的,人民法院不予受理;已经受理的,裁定驳回起诉。**
**第十二条 劳动争议仲裁机构逾期未作出受理决定或仲裁裁决,当事人直接提起诉讼的,人民法院应予受理,但申请仲裁的案件存在下列事由的除外:**
(一)移送管辖的;
(二)正在送达或者送达延误的;
(三)等待另案诉讼结果、评残结论的;
(四)正在等待劳动争议仲裁机构开庭的;
(五)启动鉴定程序或者委托其他部门调查取证的;
(六)其他正当事由。
当事人以劳动争议仲裁机构逾期未作出仲裁裁决为由提起诉讼的,应当提交该仲裁机构出具的受理通知书或者其他已接受仲裁申请的凭证、证明。
**第十三条 劳动者依据劳动合同法第三十条第二款和调解仲裁法第十六条规定向人民法院申请支付令,符合民事诉讼法第十七章督促程序规定的,人民法院应予受理。**
依据劳动合同法第三十条第二款规定申请支付令被人民法院裁定终结督促程序后,劳动者就劳动争议事项直接提起诉讼的,人民法院应当告知其先向劳动争议仲裁机构申请仲裁。
依据调解仲裁法第十六条规定申请支付令被人民法院裁定终结督促程序后,劳动者依据调解协议直接提起诉讼的,人民法院应予受理。
**第十四条 人民法院受理劳动争议案件后,当事人增加诉讼请求的,如该诉讼请求与讼争的劳动争议具有不可分性,应当合并审理;如属独立的劳动争议,应当告知当事人向劳动争议仲裁机构申请仲裁。**
**第十五条 劳动者以用人单位的工资欠条为证据直接提起诉讼,诉讼请求不涉及劳动关系其他争议的,视为拖欠劳动报酬争议,人民法院按照普通民事纠纷受理。**
**第十六条 劳动争议仲裁机构作出仲裁裁决后,当事人对裁决中的部分事项不服,依法提起诉讼的,劳动争议仲裁裁决不发生法律效力。**
**第十七条 劳动争议仲裁机构对多个劳动者的劳动争议作出仲裁裁决后,部分劳动者对仲裁裁决不服,依法提起诉讼的,仲裁裁决对提起诉讼的劳动者不发生法律效力;对未提起诉讼的部分劳动者,发生法律效力,如其申请执行的,人民法院应当受理。**
**第十八条 仲裁裁决的类型以仲裁裁决书确定为准。仲裁裁决书未载明该裁决为终局裁决或者非终局裁决,用人单位不服该仲裁裁决向基层人民法院提起诉讼的,应当按照以下情形分别处理:**
(一)经审查认为该仲裁裁决为非终局裁决的,基层人民法院应予受理;
(二)经审查认为该仲裁裁决为终局裁决的,基层人民法院不予受理,但应告知用人单位可以自收到不予受理裁定书之日起三十日内向劳动争议仲裁机构所在地的中级人民法院申请撤销该仲裁裁决;已经受理的,裁定驳回起诉。
**第十九条 仲裁裁决书未载明该裁决为终局裁决或者非终局裁决,劳动者依据调解仲裁法第四十七条第一项规定,追索劳动报酬、工伤医疗费、经济补偿或者赔偿金,如果仲裁裁决涉及数项,每项确定的数额均不超过当地月最低工资标准十二个月金额的,应当按照终局裁决处理。**
**第二十条 劳动争议仲裁机构作出的同一仲裁裁决同时包含终局裁决事项和非终局裁决事项,当事人不服该仲裁裁决向人民法院提起诉讼的,应当按照非终局裁决处理。**
**第二十一条 劳动者依据调解仲裁法第四十八条规定向基层人民法院提起诉讼,用人单位依据调解仲裁法第四十九条规定向劳动争议仲裁机构所在地的中级人民法院申请撤销仲裁裁决的,中级人民法院应当不予受理;已经受理的,应当裁定驳回申请。**
被人民法院驳回起诉或者劳动者撤诉的,用人单位可以自收到裁定书之日起三十日内,向劳动争议仲裁机构所在地的中级人民法院申请撤销仲裁裁决。
**第二十二条 用人单位依据调解仲裁法第四十九条规定向中级人民法院申请撤销仲裁裁决,中级人民法院作出的驳回申请或者撤销仲裁裁决的裁定为终审裁定。**
**第二十三条 中级人民法院审理用人单位申请撤销终局裁决的案件,应当组成合议庭开庭审理。经过阅卷、调查和询问当事人,对没有新的事实、证据或者理由,合议庭认为不需要开庭审理的,可以不开庭审理。**
中级人民法院可以组织双方当事人调解。达成调解协议的,可以制作调解书。一方当事人逾期不履行调解协议的,另一方可以申请人民法院强制执行。
**第二十四条 当事人申请人民法院执行劳动争议仲裁机构作出的发生法律效力的裁决书、调解书,被申请人提出证据证明劳动争议仲裁裁决书、调解书有下列情形之一,并经审查核实的,人民法院可以根据民事诉讼法第二百三十七条规定,裁定不予执行:**
(一)裁决的事项不属于劳动争议仲裁范围,或者劳动争议仲裁机构无权仲裁的;
(二)适用法律、法规确有错误的;
(三)违反法定程序的;
(四)裁决所根据的证据是伪造的;
(五)对方当事人隐瞒了足以影响公正裁决的证据的;
(六)仲裁员在仲裁该案时有索贿受贿、徇私舞弊、枉法裁决行为的;
(七)人民法院认定执行该劳动争议仲裁裁决违背社会公共利益的。
人民法院在不予执行的裁定书中,应当告知当事人在收到裁定书之次日起三十日内,可以就该劳动争议事项向人民法院提起诉讼。
**第二十五条 劳动争议仲裁机构作出终局裁决,劳动者向人民法院申请执行,用人单位向劳动争议仲裁机构所在地的中级人民法院申请撤销的,人民法院应当裁定中止执行。**
用人单位撤回撤销终局裁决申请或者其申请被驳回的,人民法院应当裁定恢复执行。仲裁裁决被撤销的,人民法院应当裁定终结执行。
用人单位向人民法院申请撤销仲裁裁决被驳回后,又在执行程序中以相同理由提出不予执行抗辩的,人民法院不予支持。
**第二十六条 用人单位与其它单位合并的,合并前发生的劳动争议,由合并后的单位为当事人;用人单位分立为若干单位的,其分立前发生的劳动争议,由分立后的实际用人单位为当事人。**
用人单位分立为若干单位后,具体承受劳动权利义务的单位不明确的,分立后的单位均为当事人。
**第二十七条 用人单位招用尚未解除劳动合同的劳动者,原用人单位与劳动者发生的劳动争议,可以列新的用人单位为第三人。**
原用人单位以新的用人单位侵权为由提起诉讼的,可以列劳动者为第三人。
原用人单位以新的用人单位和劳动者共同侵权为由提起诉讼的,新的用人单位和劳动者列为共同被告。
**第二十八条 劳动者在用人单位与其他平等主体之间的承包经营期间,与发包方和承包方双方或者一方发生劳动争议,依法提起诉讼的,应当将承包方和发包方作为当事人。**
**第二十九条 劳动者与未办理营业执照、营业执照被吊销或者营业期限届满仍继续经营的用人单位发生争议的,应当将用人单位或者其出资人列为当事人。**
**第三十条 未办理营业执照、营业执照被吊销或者营业期限届满仍继续经营的用人单位,以挂靠等方式借用他人营业执照经营的,应当将用人单位和营业执照出借方列为当事人。**
**第三十一条 当事人不服劳动争议仲裁机构作出的仲裁裁决,依法提起诉讼,人民法院审查认为仲裁裁决遗漏了必须共同参加仲裁的当事人的,应当依法追加遗漏的人为诉讼当事人。**
被追加的当事人应当承担责任的,人民法院应当一并处理。
**第三十二条 用人单位与其招用的已经依法享受养老保险待遇或者领取退休金的人员发生用工争议而提起诉讼的,人民法院应当按劳务关系处理。(第三十二条第一款已根据司法解释二废止)**
企业停薪留职人员、未达到法定退休年龄的内退人员、下岗待岗人员以及企业经营性停产放长假人员,因与新的用人单位发生用工争议而提起诉讼的,人民法院应当按劳动关系处理。
**第三十三条 外国人、无国籍人未依法取得就业证件即与中华人民共和国境内的用人单位签订劳动合同,当事人请求确认与用人单位存在劳动关系的,人民法院不予支持。**
持有《外国专家证》并取得《外国人来华工作许可证》的外国人,与中华人民共和国境内的用人单位建立用工关系的,可以认定为劳动关系。
**第三十四条 劳动合同期满后,劳动者仍在原用人单位工作,原用人单位未表示异议的,视为双方同意以原条件继续履行劳动合同。一方提出终止劳动关系的,人民法院应予支持。**
根据劳动合同法第十四条规定,用人单位应当与劳动者签订无固定期限劳动合同而未签订的,人民法院可以视为双方之间存在无固定期限劳动合同关系,并以原劳动合同确定双方的权利义务关系。
**第三十五条 劳动者与用人单位就解除或者终止劳动合同办理相关手续、支付工资报酬、加班费、经济补偿或者赔偿金等达成的协议,不违反法律、行政法规的强制性规定,且不存在欺诈、胁迫或者乘人之危情形的,应当认定有效。**
前款协议存在重大误解或者显失公平情形,当事人请求撤销的,人民法院应予支持。
**第三十六条 当事人在劳动合同或者保密协议中约定了竞业限制,但未约定解除或者终止劳动合同后给予劳动者经济补偿,劳动者履行了竞业限制义务,要求用人单位按照劳动者在劳动合同解除或者终止前十二个月平均工资的30%按月支付经济补偿的,人民法院应予支持。**
前款规定的月平均工资的30%低于劳动合同履行地最低工资标准的,按照劳动合同履行地最低工资标准支付。
**第三十七条 当事人在劳动合同或者保密协议中约定了竞业限制和经济补偿,当事人解除劳动合同时,除另有约定外,用人单位要求劳动者履行竞业限制义务,或者劳动者履行了竞业限制义务后要求用人单位支付经济补偿的,人民法院应予支持。**
**第三十八条 当事人在劳动合同或者保密协议中约定了竞业限制和经济补偿,劳动合同解除或者终止后,因用人单位的原因导致三个月未支付经济补偿,劳动者请求解除竞业限制约定的,人民法院应予支持。**
**第三十九条 在竞业限制期限内,用人单位请求解除竞业限制协议的,人民法院应予支持。**
在解除竞业限制协议时,劳动者请求用人单位额外支付劳动者三个月的竞业限制经济补偿的,人民法院应予支持。
**第四十条 劳动者违反竞业限制约定,向用人单位支付违约金后,用人单位要求劳动者按照约定继续履行竞业限制义务的,人民法院应予支持。**
**第四十一条 劳动合同被确认为无效,劳动者已付出劳动的,用人单位应当按照劳动合同法第二十八条、第四十六条、第四十七条的规定向劳动者支付劳动报酬和经济补偿。**
由于用人单位原因订立无效劳动合同,给劳动者造成损害的,用人单位应当赔偿劳动者因合同无效所造成的经济损失。
**第四十二条 劳动者主张加班费的,应当就加班事实的存在承担举证责任。但劳动者有证据证明用人单位掌握加班事实存在的证据,用人单位不提供的,由用人单位承担不利后果。**
**第四十三条 用人单位与劳动者协商一致变更劳动合同,虽未采用书面形式,但已经实际履行了口头变更的劳动合同超过一个月,变更后的劳动合同内容不违反法律、行政法规且不违背公序良俗,当事人以未采用书面形式为由主张劳动合同变更无效的,人民法院不予支持。**
**第四十四条 因用人单位作出的开除、除名、辞退、解除劳动合同、减少劳动报酬、计算劳动者工作年限等决定而发生的劳动争议,用人单位负举证责任。**
**第四十五条 用人单位有下列情形之一,迫使劳动者提出解除劳动合同的,用人单位应当支付劳动者的劳动报酬和经济补偿,并可支付赔偿金:**
(一)以暴力、威胁或者非法限制人身自由的手段强迫劳动的;
(二)未按照劳动合同约定支付劳动报酬或者提供劳动条件的;
(三)克扣或者无故拖欠劳动者工资的;
(四)拒不支付劳动者延长工作时间工资报酬的;
(五)低于当地最低工资标准支付劳动者工资的。
**第四十六条 劳动者非因本人原因从原用人单位被安排到新用人单位工作,原用人单位未支付经济补偿,劳动者依据劳动合同法第三十八条规定与新用人单位解除劳动合同,或者新用人单位向劳动者提出解除、终止劳动合同,在计算支付经济补偿或赔偿金的工作年限时,劳动者请求把在原用人单位的工作年限合并计算为新用人单位工作年限的,人民法院应予支持。**
用人单位符合下列情形之一的,应当认定属于“劳动者非因本人原因从原用人单位被安排到新用人单位工作”:
(一)劳动者仍在原工作场所、工作岗位工作,劳动合同主体由原用人单位变更为新用人单位;
(二)用人单位以组织委派或任命形式对劳动者进行工作调动;
(三)因用人单位合并、分立等原因导致劳动者工作调动;
(四)用人单位及其关联企业与劳动者轮流订立劳动合同;
(五)其他合理情形。
**第四十七条 建立了工会组织的用人单位解除劳动合同符合劳动合同法第三十九条、第四十条规定,但未按照劳动合同法第四十三条规定事先通知工会,劳动者以用人单位违法解除劳动合同为由请求用人单位支付赔偿金的,人民法院应予支持,但起诉前用人单位已经补正有关程序的除外。**
**第四十八条 劳动合同法施行后,因用人单位经营期限届满不再继续经营导致劳动合同不能继续履行,劳动者请求用人单位支付经济补偿的,人民法院应予支持。**
**第四十九条 在诉讼过程中,劳动者向人民法院申请采取财产保全措施,人民法院经审查认为申请人经济确有困难,或者有证据证明用人单位存在欠薪逃匿可能的,应当减轻或者免除劳动者提供担保的义务,及时采取保全措施。**
人民法院作出的财产保全裁定中,应当告知当事人在劳动争议仲裁机构的裁决书或者在人民法院的裁判文书生效后三个月内申请强制执行。逾期不申请的,人民法院应当裁定解除保全措施。
**第五十条 用人单位根据劳动合同法第四条规定,通过民主程序制定的规章制度,不违反国家法律、行政法规及政策规定,并已向劳动者公示的,可以作为确定双方权利义务的依据。**
用人单位制定的内部规章制度与集体合同或者劳动合同约定的内容不一致,劳动者请求优先适用合同约定的,人民法院应予支持。
**第五十一条 当事人在调解仲裁法第十条规定的调解组织主持下达成的具有劳动权利义务内容的调解协议,具有劳动合同的约束力,可以作为人民法院裁判的根据。**
当事人在调解仲裁法第十条规定的调解组织主持下仅就劳动报酬争议达成调解协议,用人单位不履行调解协议确定的给付义务,劳动者直接提起诉讼的,人民法院可以按照普通民事纠纷受理。
**第五十二条 当事人在人民调解委员会主持下仅就给付义务达成的调解协议,双方认为有必要的,可以共同向人民调解委员会所在地的基层人民法院申请司法确认。**
**第五十三条 用人单位对劳动者作出的开除、除名、辞退等处理,或者因其他原因解除劳动合同确有错误的,人民法院可以依法判决予以撤销。**
对于追索劳动报酬、养老金、医疗费以及工伤保险待遇、经济补偿金、培训费及其他相关费用等案件,给付数额不当的,人民法院可以予以变更。
**第五十四条 本解释自2021年1月1日起施行。**
FILE:references/complete/司法解释(二).md
# 最高人民法院关于审理劳动争议案件适用法律问题的解释(二)(完整版)
最高人民法院关于审理劳动争议案件适用法律问题的解释(二)
法释〔2025〕12号
(2025年2月17日最高人民法院审判委员会第1942次会议通过,自2025年9月1日起施行)
为正确审理劳动争议案件,根据《中华人民共和国民法典》《中华人民共和国劳动法》《中华人民共和国劳动合同法》《中华人民共和国民事诉讼法》《中华人民共和国劳动争议调解仲裁法》等相关法律规定,结合审判实践,制定本解释。
**第一条 具备合法经营资格的承包人将承包业务转包或者分包给不具备合法经营资格的组织或者个人,该组织或者个人招用的劳动者请求确认承包人为承担用工主体责任单位,承担支付劳动报酬、认定工伤后的工伤保险待遇等责任的,人民法院依法予以支持。**
**第二条 不具备合法经营资格的组织或者个人挂靠具备合法经营资格的单位对外经营,该组织或者个人招用的劳动者请求确认被挂靠单位为承担用工主体责任单位,承担支付劳动报酬、认定工伤后的工伤保险待遇等责任的,人民法院依法予以支持。**
**第三条 劳动者被多个存在关联关系的单位交替或者同时用工,其请求确认劳动关系的,人民法院按照下列情形分别处理:**
(一)已订立书面劳动合同,劳动者请求按照劳动合同确认劳动关系的,人民法院依法予以支持;
(二)未订立书面劳动合同的,根据用工管理行为,综合考虑工作时间、工作内容、劳动报酬支付、社会保险费缴纳等因素确认劳动关系。
劳动者请求符合前款第二项规定情形的关联单位共同承担支付劳动报酬、福利待遇等责任的,人民法院依法予以支持,但关联单位之间依法对劳动者的劳动报酬、福利待遇等作出约定且经劳动者同意的除外。
**第四条 外国人与中华人民共和国境内的用人单位建立用工关系,有下列情形之一,外国人请求确认与用人单位存在劳动关系的,人民法院依法予以支持:**
(一)已取得永久居留资格的;
(二)已取得工作许可且在中国境内合法停留居留的;
(三)按照国家有关规定办理相关手续的。
**第五条 依法设立的外国企业常驻代表机构可以作为劳动争议案件的当事人。当事人申请追加外国企业参加诉讼的,人民法院依法予以支持。**
**第六条 用人单位未依法与劳动者订立书面劳动合同,应当支付劳动者的二倍工资按月计算;不满一个月的,按该月实际工作日计算。**
**第七条 劳动者以用人单位未订立书面劳动合同为由,请求用人单位支付二倍工资的,人民法院依法予以支持,但用人单位举证证明存在下列情形之一的除外:**
(一)因不可抗力导致未订立的;
(二)因劳动者本人故意或者重大过失未订立的;
(三)法律、行政法规规定的其他情形。
**第八条 劳动合同期满,有下列情形之一的,人民法院认定劳动合同期限依法自动续延,不属于用人单位未订立书面劳动合同的情形:**
(一)劳动合同法第四十二条规定的用人单位不得解除劳动合同的;
(二)劳动合同法实施条例第十七条规定的服务期尚未到期的;
(三)工会法第十九条规定的任期未届满的。
**第九条 有证据证明存在劳动合同法第十四条第三款规定的“视为用人单位与劳动者已订立无固定期限劳动合同”情形,劳动者请求与用人单位订立书面劳动合同的,人民法院依法予以支持;劳动者以用人单位未及时补订书面劳动合同为由,请求用人单位支付视为已与劳动者订立无固定期限劳动合同期间二倍工资的,人民法院不予支持。**
**第十条 有下列情形之一的,人民法院应认定为符合劳动合同法第十四条第二款第三项“连续订立二次固定期限劳动合同”的规定:**
(一)用人单位与劳动者协商延长劳动合同期限累计达到一年以上,延长期限届满的;
(二)用人单位与劳动者约定劳动合同期满后自动续延,续延期限届满的;
(三)劳动者非因本人原因仍在原工作场所、工作岗位工作,用人单位变换劳动合同订立主体,但继续对劳动者进行劳动管理,合同期限届满的;
(四)以其他违反诚信原则的规避行为再次订立劳动合同,期限届满的。
**第十一条 劳动合同期满后,劳动者仍在用人单位工作,用人单位未表示异议超过一个月,劳动者请求用人单位以原条件续订劳动合同的,人民法院依法予以支持。**
符合订立无固定期限劳动合同情形,劳动者请求用人单位以原条件订立无固定期限劳动合同的,人民法院依法予以支持。
用人单位解除劳动合同,劳动者请求用人单位依法承担解除劳动合同法律后果的,人民法院依法予以支持。
**第十二条 除向劳动者支付正常劳动报酬外,用人单位与劳动者约定服务期限并提供特殊待遇,劳动者违反约定提前解除劳动合同且不符合劳动合同法第三十八条规定的单方解除劳动合同情形时,用人单位请求劳动者承担赔偿损失责任的,人民法院可以综合考虑实际损失、当事人的过错程度、已经履行的年限等因素确定劳动者应当承担的赔偿责任。**
**第十三条 劳动者未知悉、接触用人单位的商业秘密和与知识产权相关的保密事项,劳动者请求确认竞业限制条款不生效的,人民法院依法予以支持。**
竞业限制条款约定的竞业限制范围、地域、期限等内容与劳动者知悉、接触的商业秘密和与知识产权相关的保密事项不相适应,劳动者请求确认竞业限制条款超过合理比例部分无效的,人民法院依法予以支持。
**第十四条 用人单位与高级管理人员、高级技术人员和其他负有保密义务的人员约定在职期间竞业限制条款,劳动者以不得约定在职期间竞业限制、未支付经济补偿为由请求确认竞业限制条款无效的,人民法院不予支持。**
**第十五条 劳动者违反有效的竞业限制约定,用人单位请求劳动者按照约定返还已经支付的经济补偿并支付违约金的,人民法院依法予以支持。**
**第十六条 用人单位违法解除或者终止劳动合同后,有下列情形之一的,人民法院可以认定为劳动合同法第四十八条规定的“劳动合同已经不能继续履行”:**
(一)劳动合同在仲裁或者诉讼过程中期满且不存在应当依法续订、续延劳动合同情形的;
(二)劳动者开始依法享受基本养老保险待遇的;
(三)用人单位被宣告破产的;
(四)用人单位解散的,但因合并或者分立需要解散的除外;
(五)劳动者已经与其他用人单位建立劳动关系,对完成用人单位的工作任务造成严重影响,或者经用人单位提出,不与其他用人单位解除劳动合同的;
(六)存在劳动合同客观不能履行的其他情形的。
**第十七条 用人单位未按照国务院安全生产监督管理部门、卫生行政部门的规定组织从事接触职业病危害作业的劳动者进行离岗前的职业健康检查,劳动者在双方解除劳动合同后请求继续履行劳动合同的,人民法院依法予以支持,但有下列情形之一的除外:**
(一)一审法庭辩论终结前,用人单位已经组织劳动者进行职业健康检查且经检查劳动者未患职业病的;
(二)一审法庭辩论终结前,用人单位组织劳动者进行职业健康检查,劳动者无正当理由拒绝检查的。
**第十八条 用人单位违法解除、终止可以继续履行的劳动合同,劳动者请求用人单位支付违法解除、终止决定作出后至劳动合同继续履行前一日工资的,用人单位应当按照劳动者提供正常劳动时的工资标准向劳动者支付上述期间的工资。**
用人单位、劳动者对于劳动合同解除、终止都有过错的,应当各自承担相应的责任。
**第十九条 用人单位与劳动者约定或者劳动者向用人单位承诺无需缴纳社会保险费的,人民法院应当认定该约定或者承诺无效。用人单位未依法缴纳社会保险费,劳动者根据劳动合同法第三十八条第一款第三项规定请求解除劳动合同、由用人单位支付经济补偿的,人民法院依法予以支持。**
有前款规定情形,用人单位依法补缴社会保险费后,请求劳动者返还已支付的社会保险费补偿的,人民法院依法予以支持。
**第二十条 当事人在仲裁期间因自身原因未提出仲裁时效抗辩,在一审或者二审诉讼期间提出仲裁时效抗辩的,人民法院不予支持。当事人基于新的证据能够证明对方当事人请求权的仲裁时效期间届满的,人民法院应予支持。**
当事人未按照前款规定提出仲裁时效抗辩,以仲裁时效期间届满为由申请再审或者提出再审抗辩的,人民法院不予支持。
**第二十一条 本解释自2025年9月1日起施行。《最高人民法院关于审理劳动争议案件适用法律问题的解释(一)》(法释〔2020〕26号)第三十二条第一款同时废止。最高人民法院此前发布的司法解释与本解释不一致的,以本解释为准。**
FILE:references/complete/工伤保险条例.md
# 工伤保险条例(完整版)
工伤保险条例
(2003年4月27日中华人民共和国国务院令第375号公布 根据2010年12月20日《国务院关于修改〈工伤保险条例〉的决定》修订)
## 第一章 总则
**第一条 为了保障因工作遭受事故伤害或者患职业病的职工获得医疗救治和经济补偿,促进工伤预防和职业康复,分散用人单位的工伤风险,制定本条例。**
**第二条 中华人民共和国境内的企业、事业单位、社会团体、民办非企业单位、基金会、律师事务所、会计师事务所等组织和有雇工的个体工商户(以下称用人单位)应当依照本条例规定参加工伤保险,为本单位全部职工或者雇工(以下称职工)缴纳工伤保险费。**
中华人民共和国境内的企业、事业单位、社会团体、民办非企业单位、基金会、律师事务所、会计师事务所等组织的职工和个体工商户的雇工,均有依照本条例的规定享受工伤保险待遇的权利。
**第三条 工伤保险费的征缴按照《社会保险费征缴暂行条例》关于基本养老保险费、基本医疗保险费、失业保险费的征缴规定执行。**
**第四条 用人单位应当将参加工伤保险的有关情况在本单位内公示。**
用人单位和职工应当遵守有关安全生产和职业病防治的法律法规,执行安全卫生规程和标准,预防工伤事故发生,避免和减少职业病危害。
职工发生工伤时,用人单位应当采取措施使工伤职工得到及时救治。
**第五条 国务院社会保险行政部门负责全国的工伤保险工作。**
县级以上地方各级人民政府社会保险行政部门负责本行政区域内的工伤保险工作。
社会保险行政部门按照国务院有关规定设立的社会保险经办机构(以下称经办机构)具体承办工伤保险事务。
**第六条 社会保险行政部门等部门制定工伤保险的政策、标准,应当征求工会组织、用人单位代表的意见。**
## 第二章 工伤保险基金
**第七条 工伤保险基金由用人单位缴纳的工伤保险费、工伤保险基金的利息和依法纳入工伤保险基金的其他资金构成。**
**第八条 工伤保险费根据以支定收、收支平衡的原则,确定费率。**
国家根据不同行业的工伤风险程度确定行业的差别费率,并根据工伤保险费使用、工伤发生率等情况在每个行业内确定若干费率档次。行业差别费率及行业内费率档次由国务院社会保险行政部门制定,报国务院批准后公布施行。
统筹地区经办机构根据用人单位工伤保险费使用、工伤发生率等情况,适用所属行业内相应的费率档次确定单位缴费费率。
**第九条 国务院社会保险行政部门应当定期了解全国各统筹地区工伤保险基金收支情况,及时提出调整行业差别费率及行业内费率档次的方案,报国务院批准后公布施行。**
**第十条 用人单位应当按时缴纳工伤保险费。职工个人不缴纳工伤保险费。**
用人单位缴纳工伤保险费的数额为本单位职工工资总额乘以单位缴费费率之积。
对难以按照工资总额缴纳工伤保险费的行业,其缴纳工伤保险费的具体方式,由国务院社会保险行政部门规定。
**第十一条 工伤保险基金逐步实行省级统筹。**
跨地区、生产流动性较大的行业,可以采取相对集中的方式异地参加统筹地区的工伤保险。具体办法由国务院社会保险行政部门会同有关行业的主管部门制定。
**第十二条 工伤保险基金存入社会保障基金财政专户,用于本条例规定的工伤保险待遇,劳动能力鉴定,工伤预防的宣传、培训等费用,以及法律、法规规定的用于工伤保险的其他费用的支付。**
工伤预防费用的提取比例、使用和管理的具体办法,由国务院社会保险行政部门会同国务院财政、卫生行政、安全生产监督管理等部门规定。
任何单位或者个人不得将工伤保险基金用于投资运营、兴建或者改建办公场所、发放奖金,或者挪作其他用途。
**第十三条 工伤保险基金应当留有一定比例的储备金,用于统筹地区重大事故的工伤保险待遇支付;储备金不足支付的,由统筹地区的人民政府垫付。储备金占基金总额的具体比例和储备金的使用办法,由省、自治区、直辖市人民政府规定。**
## 第三章 工伤认定
**第十四条 职工有下列情形之一的,应当认定为工伤:**
(一)在工作时间和工作场所内,因工作原因受到事故伤害的;
(二)工作时间前后在工作场所内,从事与工作有关的预备性或者收尾性工作受到事故伤害的;
(三)在工作时间和工作场所内,因履行工作职责受到暴力等意外伤害的;
(四)患职业病的;
(五)因工外出期间,由于工作原因受到伤害或者发生事故下落不明的;
(六)在上下班途中,受到非本人主要责任的交通事故或者城市轨道交通、客运轮渡、火车事故伤害的;
(七)法律、行政法规规定应当认定为工伤的其他情形。
**第十五条 职工有下列情形之一的,视同工伤:**
(一)在工作时间和工作岗位,突发疾病死亡或者在48小时之内经抢救无效死亡的;
(二)在抢险救灾等维护国家利益、公共利益活动中受到伤害的;
(三)职工原在军队服役,因战、因公负伤致残,已取得革命伤残军人证,到用人单位后旧伤复发的。
职工有前款第(一)项、第(二)项情形的,按照本条例的有关规定享受工伤保险待遇;职工有前款第(三)项情形的,按照本条例的有关规定享受除一次性伤残补助金以外的工伤保险待遇。
**第十六条 职工符合本条例第十四条、第十五条的规定,但是有下列情形之一的,不得认定为工伤或者视同工伤:**
(一)故意犯罪的;
(二)醉酒或者吸毒的;
(三)自残或者自杀的。
**第十七条 职工发生事故伤害或者按照职业病防治法规定被诊断、鉴定为职业病,所在单位应当自事故伤害发生之日或者被诊断、鉴定为职业病之日起30日内,向统筹地区社会保险行政部门提出工伤认定申请。遇有特殊情况,经报社会保险行政部门同意,申请时限可以适当延长。**
用人单位未按前款规定提出工伤认定申请的,工伤职工或者其近亲属、工会组织在事故伤害发生之日或者被诊断、鉴定为职业病之日起1年内,可以直接向用人单位所在地统筹地区社会保险行政部门提出工伤认定申请。
按照本条第一款规定应当由省级社会保险行政部门进行工伤认定的事项,根据属地原则由用人单位所在地的设区的市级社会保险行政部门办理。
用人单位未在本条第一款规定的时限内提交工伤认定申请,在此期间发生符合本条例规定的工伤待遇等有关费用由该用人单位负担。
**第十八条 提出工伤认定申请应当提交下列材料:**
(一)工伤认定申请表;
(二)与用人单位存在劳动关系(包括事实劳动关系)的证明材料;
(三)医疗诊断证明或者职业病诊断证明书(或者职业病诊断鉴定书)。
工伤认定申请表应当包括事故发生的时间、地点、原因以及职工伤害程度等基本情况。
工伤认定申请人提供材料不完整的,社会保险行政部门应当一次性书面告知工伤认定申请人需要补正的全部材料。申请人按照书面告知要求补正材料后,社会保险行政部门应当受理。
**第十九条 社会保险行政部门受理工伤认定申请后,根据审核需要可以对事故伤害进行调查核实,用人单位、职工、工会组织、医疗机构以及有关部门应当予以协助。职业病诊断和诊断争议的鉴定,依照职业病防治法的有关规定执行。对依法取得职业病诊断证明书或者职业病诊断鉴定书的,社会保险行政部门不再进行调查核实。**
职工或者其近亲属认为是工伤,用人单位不认为是工伤的,由用人单位承担举证责任。
**第二十条 社会保险行政部门应当自受理工伤认定申请之日起60日内作出工伤认定的决定,并书面通知申请工伤认定的职工或者其近亲属和该职工所在单位。**
社会保险行政部门对受理的事实清楚、权利义务明确的工伤认定申请,应当在15日内作出工伤认定的决定。
作出工伤认定决定需要以司法机关或者有关行政主管部门的结论为依据的,在司法机关或者有关行政主管部门尚未作出结论期间,作出工伤认定决定的时限中止。
社会保险行政部门工作人员与工伤认定申请人有利害关系的,应当回避。
## 第四章 劳动能力鉴定
**第二十一条 职工发生工伤,经治疗伤情相对稳定后存在残疾、影响劳动能力的,应当进行劳动能力鉴定。**
**第二十二条 劳动能力鉴定是指劳动功能障碍程度和生活自理障碍程度的等级鉴定。**
劳动功能障碍分为十个伤残等级,最重的为一级,最轻的为十级。
生活自理障碍分为三个等级:生活完全不能自理、生活大部分不能自理和生活部分不能自理。
劳动能力鉴定标准由国务院社会保险行政部门会同国务院卫生行政部门等部门制定。
**第二十三条 劳动能力鉴定由用人单位、工伤职工或者其近亲属向设区的市级劳动能力鉴定委员会提出申请,并提供工伤认定决定和职工工伤医疗的有关资料。**
**第二十四条 省、自治区、直辖市劳动能力鉴定委员会和设区的市级劳动能力鉴定委员会分别由省、自治区、直辖市和设区的市级社会保险行政部门、卫生行政部门、工会组织、经办机构代表以及用人单位代表组成。**
劳动能力鉴定委员会建立医疗卫生专家库。列入专家库的医疗卫生专业技术人员应当具备下列条件:
(一)具有医疗卫生高级专业技术职务任职资格;
(二)掌握劳动能力鉴定的相关知识;
(三)具有良好的职业品德。
**第二十五条 设区的市级劳动能力鉴定委员会收到劳动能力鉴定申请后,应当从其建立的医疗卫生专家库中随机抽取3名或者5名相关专家组成专家组,由专家组提出鉴定意见。设区的市级劳动能力鉴定委员会根据专家组的鉴定意见作出工伤职工劳动能力鉴定结论;必要时,可以委托具备资格的医疗机构协助进行有关的诊断。**
设区的市级劳动能力鉴定委员会应当自收到劳动能力鉴定申请之日起60日内作出劳动能力鉴定结论,必要时,作出劳动能力鉴定结论的期限可以延长30日。劳动能力鉴定结论应当及时送达申请鉴定的单位和个人。
**第二十六条 申请鉴定的单位或者个人对设区的市级劳动能力鉴定委员会作出的鉴定结论不服的,可以在收到该鉴定结论之日起15日内向省、自治区、直辖市劳动能力鉴定委员会提出再次鉴定申请。省、自治区、直辖市劳动能力鉴定委员会作出的劳动能力鉴定结论为最终结论。**
**第二十七条 劳动能力鉴定工作应当客观、公正。劳动能力鉴定委员会组成人员或者参加鉴定的专家与当事人有利害关系的,应当回避。**
**第二十八条 自劳动能力鉴定结论作出之日起1年后,工伤职工或者其近亲属、所在单位或者经办机构认为伤残情况发生变化的,可以申请劳动能力复查鉴定。**
**第二十九条 劳动能力鉴定委员会依照本条例第二十六条和第二十八条的规定进行再次鉴定和复查鉴定的期限,依照本条例第二十五条第二款的规定执行。**
## 第五章 工伤保险待遇
**第三十条 职工因工作遭受事故伤害或者患职业病进行治疗,享受工伤医疗待遇。**
职工治疗工伤应当在签订服务协议的医疗机构就医,情况紧急时可以先到就近的医疗机构急救。
治疗工伤所需费用符合工伤保险诊疗项目目录、工伤保险药品目录、工伤保险住院服务标准的,从工伤保险基金支付。工伤保险诊疗项目目录、工伤保险药品目录、工伤保险住院服务标准,由国务院社会保险行政部门会同国务院卫生行政部门、食品药品监督管理部门等部门规定。
职工住院治疗工伤的伙食补助费,以及经医疗机构出具证明,报经办机构同意,工伤职工到统筹地区以外就医所需的交通、食宿费用从工伤保险基金支付,基金支付的具体标准由统筹地区人民政府规定。
工伤职工治疗非工伤引发的疾病,不享受工伤医疗待遇,按照基本医疗保险办法处理。
工伤职工到签订服务协议的医疗机构进行工伤康复的费用,符合规定的,从工伤保险基金支付。
**第三十一条 社会保险行政部门作出认定为工伤的决定后发生行政复议、行政诉讼的,行政复议和行政诉讼期间不停止支付工伤职工治疗工伤的医疗费用。**
**第三十二条 工伤职工因日常生活或者就业需要,经劳动能力鉴定委员会确认,可以安装假肢、矫形器、假眼、假牙和配置轮椅等辅助器具,所需费用按照国家规定的标准从工伤保险基金支付。**
**第三十三条 职工因工作遭受事故伤害或者患职业病需要暂停工作接受工伤医疗的,在停工留薪期内,原工资福利待遇不变,由所在单位按月支付。**
停工留薪期一般不超过12个月。伤情严重或者情况特殊,经设区的市级劳动能力鉴定委员会确认,可以适当延长,但延长不得超过12个月。工伤职工评定伤残等级后,停发原待遇,按照本章的有关规定享受伤残待遇。工伤职工在停工留薪期满后仍需治疗的,继续享受工伤医疗待遇。
生活不能自理的工伤职工在停工留薪期需要护理的,由所在单位负责。
**第三十四条 工伤职工已经评定伤残等级并经劳动能力鉴定委员会确认需要生活护理的,从工伤保险基金按月支付生活护理费。**
生活护理费按照生活完全不能自理、生活大部分不能自理或者生活部分不能自理3个不同等级支付,其标准分别为统筹地区上年度职工月平均工资的50%、40%或者30%。
**第三十五条 职工因工致残被鉴定为一级至四级伤残的,保留劳动关系,退出工作岗位,享受以下待遇:**
(一)从工伤保险基金按伤残等级支付一次性伤残补助金,标准为:一级伤残为27个月的本人工资,二级伤残为25个月的本人工资,三级伤残为23个月的本人工资,四级伤残为21个月的本人工资;
(二)从工伤保险基金按月支付伤残津贴,标准为:一级伤残为本人工资的90%,二级伤残为本人工资的85%,三级伤残为本人工资的80%,四级伤残为本人工资的75%。伤残津贴实际金额低于当地最低工资标准的,由工伤保险基金补足差额;
(三)工伤职工达到退休年龄并办理退休手续后,停发伤残津贴,按照国家有关规定享受基本养老保险待遇。基本养老保险待遇低于伤残津贴的,由工伤保险基金补足差额。
职工因工致残被鉴定为一级至四级伤残的,由用人单位和职工个人以伤残津贴为基数,缴纳基本医疗保险费。
**第三十六条 职工因工致残被鉴定为五级、六级伤残的,享受以下待遇:**
(一)从工伤保险基金按伤残等级支付一次性伤残补助金,标准为:五级伤残为18个月的本人工资,六级伤残为16个月的本人工资;
(二)保留与用人单位的劳动关系,由用人单位安排适当工作。难以安排工作的,由用人单位按月发给伤残津贴,标准为:五级伤残为本人工资的70%,六级伤残为本人工资的60%,并由用人单位按照规定为其缴纳应缴纳的各项社会保险费。伤残津贴实际金额低于当地最低工资标准的,由用人单位补足差额。
经工伤职工本人提出,该职工可以与用人单位解除或者终止劳动关系,由工伤保险基金支付一次性工伤医疗补助金,由用人单位支付一次性伤残就业补助金。一次性工伤医疗补助金和一次性伤残就业补助金的具体标准由省、自治区、直辖市人民政府规定。
**第三十七条 职工因工致残被鉴定为七级至十级伤残的,享受以下待遇:**
(一)从工伤保险基金按伤残等级支付一次性伤残补助金,标准为:七级伤残为13个月的本人工资,八级伤残为11个月的本人工资,九级伤残为9个月的本人工资,十级伤残为7个月的本人工资;
(二)劳动、聘用合同期满终止,或者职工本人提出解除劳动、聘用合同的,由工伤保险基金支付一次性工伤医疗补助金,由用人单位支付一次性伤残就业补助金。一次性工伤医疗补助金和一次性伤残就业补助金的具体标准由省、自治区、直辖市人民政府规定。
**第三十八条 工伤职工工伤复发,确认需要治疗的,享受本条例第三十条、第三十二条和第三十三条规定的工伤待遇。**
**第三十九条 职工因工死亡,其近亲属按照下列规定从工伤保险基金领取丧葬补助金、供养亲属抚恤金和一次性工亡补助金:**
(一)丧葬补助金为6个月的统筹地区上年度职工月平均工资;
(二)供养亲属抚恤金按照职工本人工资的一定比例发给由因工死亡职工生前提供主要生活来源、无劳动能力的亲属。标准为:配偶每月40%,其他亲属每人每月30%,孤寡老人或者孤儿每人每月在上述标准的基础上增加10%。核定的各供养亲属的抚恤金之和不应高于因工死亡职工生前的工资。供养亲属的具体范围由国务院社会保险行政部门规定;
(三)一次性工亡补助金标准为上一年度全国城镇居民人均可支配收入的20倍。
伤残职工在停工留薪期内因工伤导致死亡的,其近亲属享受本条第一款规定的待遇。
一级至四级伤残职工在停工留薪期满后死亡的,其近亲属可以享受本条第一款第(一)项、第(二)项规定的待遇。
**第四十条 伤残津贴、供养亲属抚恤金、生活护理费由统筹地区社会保险行政部门根据职工平均工资和生活费用变化等情况适时调整。调整办法由省、自治区、直辖市人民政府规定。**
**第四十一条 职工因工外出期间发生事故或者在抢险救灾中下落不明的,从事故发生当月起3个月内照发工资,从第4个月起停发工资,由工伤保险基金向其供养亲属按月支付供养亲属抚恤金。生活有困难的,可以预支一次性工亡补助金的50%。职工被人民法院宣告死亡的,按照本条例第三十九条职工因工死亡的规定处理。**
**第四十二条 工伤职工有下列情形之一的,停止享受工伤保险待遇:**
(一)丧失享受待遇条件的;
(二)拒不接受劳动能力鉴定的;
(三)拒绝治疗的。
**第四十三条 用人单位分立、合并、转让的,承继单位应当承担原用人单位的工伤保险责任;原用人单位已经参加工伤保险的,承继单位应当到当地经办机构办理工伤保险变更登记。**
用人单位实行承包经营的,工伤保险责任由职工劳动关系所在单位承担。
职工被借调期间受到工伤事故伤害的,由原用人单位承担工伤保险责任,但原用人单位与借调单位可以约定补偿办法。
企业破产的,在破产清算时依法拨付应当由单位支付的工伤保险待遇费用。
**第四十四条 职工被派遣出境工作,依据前往国家或者地区的法律应当参加当地工伤保险的,参加当地工伤保险,其国内工伤保险关系中止;不能参加当地工伤保险的,其国内工伤保险关系不中止。**
**第四十五条 职工再次发生工伤,根据规定应当享受伤残津贴的,按照新认定的伤残等级享受伤残津贴待遇。**
## 第六章 监督管理
**第四十六条 经办机构具体承办工伤保险事务,履行下列职责:**
(一)根据省、自治区、直辖市人民政府规定,征收工伤保险费;
(二)核查用人单位的工资总额和职工人数,办理工伤保险登记,并负责保存用人单位缴费和职工享受工伤保险待遇情况的记录;
(三)进行工伤保险的调查、统计;
(四)按照规定管理工伤保险基金的支出;
(五)按照规定核定工伤保险待遇;
(六)为工伤职工或者其近亲属免费提供咨询服务。
**第四十七条 经办机构与医疗机构、辅助器具配置机构在平等协商的基础上签订服务协议,并公布签订服务协议的医疗机构、辅助器具配置机构的名单。具体办法由国务院社会保险行政部门分别会同国务院卫生行政部门、民政部门等部门制定。**
**第四十八条 经办机构按照协议和国家有关目录、标准对工伤职工医疗费用、康复费用、辅助器具费用的使用情况进行核查,并按时足额结算费用。**
**第四十九条 经办机构应当定期公布工伤保险基金的收支情况,及时向社会保险行政部门提出调整费率的建议。**
**第五十条 社会保险行政部门、经办机构应当定期听取工伤职工、医疗机构、辅助器具配置机构以及社会各界对改进工伤保险工作的意见。**
**第五十一条 社会保险行政部门依法对工伤保险费的征缴和工伤保险基金的支付情况进行监督检查。**
财政部门和审计机关依法对工伤保险基金的收支、管理情况进行监督。
**第五十二条 任何组织和个人对有关工伤保险的违法行为,有权举报。社会保险行政部门对举报应当及时调查,按照规定处理,并为举报人保密。**
**第五十三条 工会组织依法维护工伤职工的合法权益,对用人单位的工伤保险工作实行监督。**
**第五十四条 职工与用人单位发生工伤待遇方面的争议,按照处理劳动争议的有关规定处理。**
**第五十五条 有下列情形之一的,有关单位或者个人可以依法申请行政复议,也可以依法向人民法院提起行政诉讼:**
(一)申请工伤认定的职工或者其近亲属、该职工所在单位对工伤认定申请不予受理的决定不服的;
(二)申请工伤认定的职工或者其近亲属、该职工所在单位对工伤认定结论不服的;
(三)用人单位对经办机构确定的单位缴费费率不服的;
(四)签订服务协议的医疗机构、辅助器具配置机构认为经办机构未履行有关协议或者规定的;
(五)工伤职工或者其近亲属对经办机构核定的工伤保险待遇有异议的。
## 第七章 法律责任
**第五十六条 单位或者个人违反本条例第十二条规定挪用工伤保险基金,构成犯罪的,依法追究刑事责任;尚不构成犯罪的,依法给予处分或者纪律处分。被挪用的基金由社会保险行政部门追回,并入工伤保险基金;没收的违法所得依法上缴国库。**
**第五十七条 社会保险行政部门工作人员有下列情形之一的,依法给予处分;情节严重,构成犯罪的,依法追究刑事责任:**
(一)无正当理由不受理工伤认定申请,或者弄虚作假将不符合工伤条件的人员认定为工伤职工的;
(二)未妥善保管申请工伤认定的证据材料,致使有关证据灭失的;
(三)收受当事人财物的。
**第五十八条 经办机构有下列行为之一的,由社会保险行政部门责令改正,对直接负责的主管人员和其他责任人员依法给予纪律处分;情节严重,构成犯罪的,依法追究刑事责任;造成当事人经济损失的,由经办机构依法承担赔偿责任:**
(一)未按规定保存用人单位缴费和职工享受工伤保险待遇情况记录的;
(二)不按规定核定工伤保险待遇的;
(三)收受当事人财物的。
**第五十九条 医疗机构、辅助器具配置机构不按服务协议提供服务的,经办机构可以解除服务协议。**
经办机构不按时足额结算费用的,由社会保险行政部门责令改正;医疗机构、辅助器具配置机构可以解除服务协议。
**第六十条 用人单位、工伤职工或者其近亲属骗取工伤保险待遇,医疗机构、辅助器具配置机构骗取工伤保险基金支出的,由社会保险行政部门责令退还,处骗取金额2倍以上5倍以下的罚款;情节严重,构成犯罪的,依法追究刑事责任。**
**第六十一条 从事劳动能力鉴定的组织或者个人有下列情形之一的,由社会保险行政部门责令改正,处2000元以上1万元以下的罚款;情节严重,构成犯罪的,依法追究刑事责任:**
(一)提供虚假鉴定意见的;
(二)提供虚假诊断证明的;
(三)收受当事人财物的。
**第六十二条 用人单位依照本条例规定应当参加工伤保险而未参加的,由社会保险行政部门责令限期参加,补缴应当缴纳的工伤保险费,并自欠缴之日起,按日加收万分之五的滞纳金;逾期仍不缴纳的,处欠缴数额1倍以上3倍以下的罚款。**
依照本条例规定应当参加工伤保险而未参加工伤保险的用人单位职工发生工伤的,由该用人单位按照本条例规定的工伤保险待遇项目和标准支付费用。
用人单位参加工伤保险并补缴应当缴纳的工伤保险费、滞纳金后,由工伤保险基金和用人单位依照本条例的规定支付新发生的费用。
**第六十三条 用人单位违反本条例第十九条的规定,拒不协助社会保险行政部门对事故进行调查核实的,由社会保险行政部门责令改正,处2000元以上2万元以下的罚款。**
## 第八章 附则
**第六十四条 本条例所称工资总额,是指用人单位直接支付给本单位全部职工的劳动报酬总额。**
本条例所称本人工资,是指工伤职工因工作遭受事故伤害或者患职业病前12个月平均月缴费工资。本人工资高于统筹地区职工平均工资300%的,按照统筹地区职工平均工资的300%计算;本人工资低于统筹地区职工平均工资60%的,按照统筹地区职工平均工资的60%计算。
**第六十五条 公务员和参照公务员法管理的事业单位、社会团体的工作人员因工作遭受事故伤害或者患职业病的,由所在单位支付费用。具体办法由国务院社会保险行政部门会同国务院财政部门规定。**
**第六十六条 无营业执照或者未经依法登记、备案的单位以及被依法吊销营业执照或者撤销登记、备案的单位的职工受到事故伤害或者患职业病的,由该单位向伤残职工或者死亡职工的近亲属给予一次性赔偿,赔偿标准不得低于本条例规定的工伤保险待遇;用人单位不得使用童工,用人单位使用童工造成童工伤残、死亡的,由该单位向童工或者童工的近亲属给予一次性赔偿,赔偿标准不得低于本条例规定的工伤保险待遇。具体办法由国务院社会保险行政部门规定。**
前款规定的伤残职工或者死亡职工的近亲属就赔偿数额与单位发生争议的,以及前款规定的童工或者童工的近亲属就赔偿数额与单位发生争议的,按照处理劳动争议的有关规定处理。
**第六十七条 本条例自2004年1月1日起施行。本条例施行前已受到事故伤害或者患职业病的职工尚未完成工伤认定的,按照本条例的规定执行。**
FILE:references/complete/工资支付暂行规定.md
# 工资支付暂行规定
> **发布机关**:中华人民共和国劳动部
> **文号**:劳部发〔1994〕489号
> **发布日期**:1994年12月6日
> **施行日期**:1995年1月1日
> **效力状态**:现行有效
---
**第一条** 为维护劳动者通过劳动获得劳动报酬的权利,规范用人单位的工资支付行为,根据《中华人民共和国劳动法》有关规定,制定本规定。
**第二条** 本规定适用于在中华人民共和国境内的企业、个体经济组织(以下统称用人单位)和与之形成劳动关系的劳动者。国家机关、事业组织、社会团体和与之建立劳动合同关系的劳动者,依照本规定执行。
**第三条** 本规定所称工资是指用人单位依据劳动合同的规定,以各种形式支付给劳动者的工资报酬。
**第四条** 工资支付主要包括:工资支付项目、工资支付水平、工资支付形式、工资支付对象、工资支付时间以及特殊情况下的工资支付。
**第五条** 工资应当以法定货币支付。不得以实物及有价证券替代货币支付。
**第六条** 用人单位应将工资支付给劳动者本人。劳动者本人因故不能领取工资时,可由其亲属或委托他人代领。用人单位可委托银行代发工资。
**第七条** 工资必须在用人单位与劳动者约定的日期支付。如遇节假日或休息日,则应提前在最近的工作日支付。工资至少每月支付一次,实行周、日、小时工资制的可按周、日、小时支付工资。
**第八条** 对完成一次性临时劳动或某项具体工作的劳动者,用人单位应按有关协议或合同规定在其完成劳动任务后即支付工资。
**第九条** 劳动关系双方依法解除或终止劳动合同时,用人单位应在解除或终止劳动合同时一次付清劳动者工资。
**第十条** 劳动者在法定工作时间内依法参加社会活动期间,用人单位应视同其提供了正常劳动而支付工资。社会活动包括:依法行使选举权或被选举权;当选代表出席乡(镇)、区以上政府、党派、工会、青年团、妇女联合会等组织召开的会议;出任人民法庭证明人;出席劳动模范、先进工作者大会;《工会法》规定的不脱产工会基层委员会委员因工作活动占用的生产或工作时间;其它依法参加的社会活动。
**第十一条** 劳动者依法享受年休假、探亲假、婚假、丧假期间,用人单位应按劳动合同规定的标准支付劳动者工资。
**第十二条** 非因劳动者原因造成单位停工、停产在一个工资支付周期内的,用人单位应按劳动合同规定的标准支付劳动者工资。超过一个工资支付周期的,若劳动者提供了正常劳动,则支付给劳动者的劳动报酬不得低于当地的最低工资标准;若劳动者没有提供正常劳动,应按国家有关规定办理。
**第十三条** 用人单位在劳动者完成劳动定额或规定的工作任务后,根据实际需要安排劳动者在法定标准工作时间以外工作的,应按以下标准支付工资:
(一)用人单位依法安排劳动者在日法定标准工作时间以外延长工作时间的,按照不低于劳动合同规定的劳动者本人小时工资标准的150%支付劳动者工资;
(二)用人单位依法安排劳动者在休息日工作,而又不能安排补休的,按照不低于劳动合同规定的劳动者本人日或小时工资标准的200%支付劳动者工资;
(三)用人单位依法安排劳动者在法定休假节日工作的,按照不低于劳动合同规定的劳动者本人日或小时工资标准的300%支付劳动者工资。
实行计件工资的劳动者,在完成计件定额任务后,由用人单位安排延长工作时间的,应根据上述规定的原则,分别按照不低于其本人法定工作时间计件单价的150%、200%、300%支付其工资。
经劳动行政部门批准实行综合计算工时工作制的,其综合计算工作时间超过法定标准工作时间的部分,应视为延长工作时间,并应按本规定支付劳动者延长工作时间的工资。
实行不定时工时制度的劳动者,不执行上述规定。
**第十四条** 用人单位依法破产时,劳动者有权获得其工资。在破产清偿中用人单位应按《中华人民共和国企业破产法》规定的清偿顺序,首先支付欠付本单位劳动者的工资。
**第十五条** 用人单位不得克扣劳动者工资。有下列情况之一的,用人单位可以代扣劳动者工资:
(一)用人单位代扣代缴的个人所得税;
(二)用人单位代扣代缴的应由劳动者个人负担的各项社会保险费用;
(三)法院判决、裁定中要求代扣的抚养费、赡养费;
(四)法律、法规规定可以从劳动者工资中扣除的其他费用。
**第十六条** 因劳动者本人原因给用人单位造成经济损失的,用人单位可按照劳动合同的约定要求其赔偿经济损失。经济损失的赔偿,可从劳动者本人的工资中扣除。但每月扣除的部分不得超过劳动者当月工资的20%。若扣除后的剩余工资部分低于当地月最低工资标准,则按最低工资标准支付。
**第十七条** 用人单位应根据本规定,通过与职工大会、职工代表大会或者其他形式协商制定内部的工资支付制度,并告知本单位全体劳动者,同时抄报当地劳动行政部门备案。
**第十八条** 各级劳动行政部门有权监察用人单位工资支付的情况。用人单位有下列侵害劳动者合法权益行为的,由劳动行政部门责令其支付劳动者工资和经济补偿,并可责令其支付赔偿金:
(一)克扣或者无故拖欠劳动者工资的;
(二)拒不支付劳动者延长工作时间工资的;
(三)低于当地最低工资标准支付劳动者工资的。
经济补偿和赔偿金的标准,按国家有关规定执行。
**第十九条** 劳动者与用人单位因工资支付发生劳动争议的,当事人可依法向劳动争议仲裁机关申请仲裁。对仲裁裁决不服的,可以向人民法院提起诉讼。
**第二十条** 本规定自一九九五年一月一日起施行。
FILE:references/simplified/劳动争议调解仲裁法.md
# 中华人民共和国劳动争议调解仲裁法(高频版)
**第二条 中华人民共和国境内的用人单位与劳动者发生的下列劳动争议,适用本法:**
(一)因确认劳动关系发生的争议;
(二)因订立、履行、变更、解除和终止劳动合同发生的争议;
(三)因除名、辞退和辞职、离职发生的争议;
(四)因工作时间、休息休假、社会保险、福利、培训以及劳动保护发生的争议;
(五)因劳动报酬、工伤医疗费、经济补偿或者赔偿金等发生的争议;
(六)法律、法规规定的其他劳动争议。
**第三条 解决劳动争议,应当根据事实,遵循合法、公正、及时、着重调解的原则,依法保护当事人的合法权益。**
**第四条 发生劳动争议,劳动者可以与用人单位协商,也可以请工会或者第三方共同与用人单位协商,达成和解协议。**
**第五条 发生劳动争议,当事人不愿协商、协商不成或者达成和解协议后不履行的,可以向调解组织申请调解;不愿调解、调解不成或者达成调解协议后不履行的,可以向劳动争议仲裁委员会申请仲裁;对仲裁裁决不服的,除本法另有规定的外,可以向人民法院提起诉讼。**
**第六条 发生劳动争议,当事人对自己提出的主张,有责任提供证据。与争议事项有关的证据属于用人单位掌握管理的,用人单位应当提供;用人单位不提供的,应当承担不利后果。**
**第七条 发生劳动争议的劳动者一方在十人以上,并有共同请求的,可以推举代表参加调解、仲裁或者诉讼活动。**
**第二十一条 劳动争议仲裁委员会负责管辖本区域内发生的劳动争议。**
劳动争议由劳动合同履行地或者用人单位所在地的劳动争议仲裁委员会管辖。双方当事人分别向劳动合同履行地和用人单位所在地的劳动争议仲裁委员会申请仲裁的,由劳动合同履行地的劳动争议仲裁委员会管辖。
**第二十二条 发生劳动争议的劳动者和用人单位为劳动争议仲裁案件的双方当事人。**
劳务派遣单位或者用工单位与劳动者发生劳动争议的,劳务派遣单位和用工单位为共同当事人。
**第二十三条 与劳动争议案件的处理结果有利害关系的第三人,可以申请参加仲裁活动或者由劳动争议仲裁委员会通知其参加仲裁活动。**
**第二十四条 当事人可以委托代理人参加仲裁活动。委托他人参加仲裁活动,应当向劳动争议仲裁委员会提交有委托人签名或者盖章的委托书,委托书应当载明委托事项和权限。**
**第二十五条 丧失或者部分丧失民事行为能力的劳动者,由其法定代理人代为参加仲裁活动;无法定代理人的,由劳动争议仲裁委员会为其指定代理人。劳动者死亡的,由其近亲属或者代理人参加仲裁活动。**
**第二十六条 劳动争议仲裁公开进行,但当事人协议不公开进行或者涉及国家秘密、商业秘密和个人隐私的除外。**
第二节 申请和受理
**第二十七条 劳动争议申请仲裁的时效期间为一年。仲裁时效期间从当事人知道或者应当知道其权利被侵害之日起计算。**
前款规定的仲裁时效,因当事人一方向对方当事人主张权利,或者向有关部门请求权利救济,或者对方当事人同意履行义务而中断。从中断时起,仲裁时效期间重新计算。
因不可抗力或者有其他正当理由,当事人不能在本条第一款规定的仲裁时效期间申请仲裁的,仲裁时效中止。从中止时效的原因消除之日起,仲裁时效期间继续计算。
劳动关系存续期间因拖欠劳动报酬发生争议的,劳动者申请仲裁不受本条第一款规定的仲裁时效期间的限制;但是,劳动关系终止的,应当自劳动关系终止之日起一年内提出。
**第二十八条 申请人申请仲裁应当提交书面仲裁申请,并按照被申请人人数提交副本。**
仲裁申请书应当载明下列事项:
(一)劳动者的姓名、性别、年龄、职业、工作单位和住所,用人单位的名称、住所和法定代表人或者主要负责人的姓名、职务;
(二)仲裁请求和所根据的事实、理由;
(三)证据和证据来源、证人姓名和住所。
书写仲裁申请确有困难的,可以口头申请,由劳动争议仲裁委员会记入笔录,并告知对方当事人。
**第二十九条 劳动争议仲裁委员会收到仲裁申请之日起五日内,认为符合受理条件的,应当受理,并通知申请人;认为不符合受理条件的,应当书面通知申请人不予受理,并说明理由。对劳动争议仲裁委员会不予受理或者逾期未作出决定的,申请人可以就该劳动争议事项向人民法院提起诉讼。**
**第四十二条 仲裁庭在作出裁决前,应当先行调解。**
调解达成协议的,仲裁庭应当制作调解书。
调解书应当写明仲裁请求和当事人协议的结果。调解书由仲裁员签名,加盖劳动争议仲裁委员会印章,送达双方当事人。调解书经双方当事人签收后,发生法律效力。
调解不成或者调解书送达前,一方当事人反悔的,仲裁庭应当及时作出裁决。
**第四十三条 仲裁庭裁决劳动争议案件,应当自劳动争议仲裁委员会受理仲裁申请之日起四十五日内结束。案情复杂需要延期的,经劳动争议仲裁委员会主任批准,可以延期并书面通知当事人,但是延长期限不得超过十五日。逾期未作出仲裁裁决的,当事人可以就该劳动争议事项向人民法院提起诉讼。**
仲裁庭裁决劳动争议案件时,其中一部分事实已经清楚,可以就该部分先行裁决。
**第四十四条 仲裁庭对追索劳动报酬、工伤医疗费、经济补偿或者赔偿金的案件,根据当事人的申请,可以裁决先予执行,移送人民法院执行。**
仲裁庭裁决先予执行的,应当符合下列条件:
(一)当事人之间权利义务关系明确;
(二)不先予执行将严重影响申请人的生活。
劳动者申请先予执行的,可以不提供担保。
**第四十七条 下列劳动争议,除本法另有规定的外,仲裁裁决为终局裁决,裁决书自作出之日起发生法律效力:**
(一)追索劳动报酬、工伤医疗费、经济补偿或者赔偿金,不超过当地月最低工资标准十二个月金额的争议;
(二)因执行国家的劳动标准在工作时间、休息休假、社会保险等方面发生的争议。
**第四十八条 劳动者对本法第四十七条规定的仲裁裁决不服的,可以自收到仲裁裁决书之日起十五日内向人民法院提起诉讼。**
**第四十九条 用人单位有证据证明本法第四十七条规定的仲裁裁决有下列情形之一,可以自收到仲裁裁决书之日起三十日内向劳动争议仲裁委员会所在地的中级人民法院申请撤销裁决:**
(一)适用法律、法规确有错误的;
(二)劳动争议仲裁委员会无管辖权的;
(三)违反法定程序的;
(四)裁决所根据的证据是伪造的;
(五)对方当事人隐瞒了足以影响公正裁决的证据的;
(六)仲裁员在仲裁该案时有索贿受贿、徇私舞弊、枉法裁决行为的。
人民法院经组成合议庭审查核实裁决有前款规定情形之一的,应当裁定撤销。
仲裁裁决被人民法院裁定撤销的,当事人可以自收到裁定书之日起十五日内就该劳动争议事项向人民法院提起诉讼。
**第五十条 当事人对本法第四十七条规定以外的其他劳动争议案件的仲裁裁决不服的,可以自收到仲裁裁决书之日起十五日内向人民法院提起诉讼;期满不起诉的,裁决书发生法律效力。**
FILE:references/simplified/劳动合同法.md
# 中华人民共和国劳动合同法(高频版)
**第二条 中华人民共和国境内的企业、个体经济组织、民办非企业单位等组织(以下称用人单位)与劳动者建立劳动关系,订立、履行、变更、解除或者终止劳动合同,适用本法。**
国家机关、事业单位、社会团体和与其建立劳动关系的劳动者,订立、履行、变更、解除或者终止劳动合同,依照本法执行。
**第四条 用人单位应当依法建立和完善劳动规章制度,保障劳动者享有劳动权利、履行劳动义务。**
用人单位在制定、修改或者决定有关劳动报酬、工作时间、休息休假、劳动安全卫生、保险福利、职工培训、劳动纪律以及劳动定额管理等直接涉及劳动者切身利益的规章制度或者重大事项时,应当经职工代表大会或者全体职工讨论,提出方案和意见,与工会或者职工代表平等协商确定。
在规章制度和重大事项决定实施过程中,工会或者职工认为不适当的,有权向用人单位提出,通过协商予以修改完善。
用人单位应当将直接涉及劳动者切身利益的规章制度和重大事项决定公示,或者告知劳动者。
**第九条 用人单位招用劳动者,不得扣押劳动者的居民身份证和其他证件,不得要求劳动者提供担保或者以其他名义向劳动者收取财物。**
**第十条 建立劳动关系,应当订立书面劳动合同。**
已建立劳动关系,未同时订立书面劳动合同的,应当自用工之日起一个月内订立书面劳动合同。
用人单位与劳动者在用工前订立劳动合同的,劳动关系自用工之日起建立。
**第十二条 劳动合同分为固定期限劳动合同、无固定期限劳动合同和以完成一定工作任务为期限的劳动合同。**
**第十三条 固定期限劳动合同,是指用人单位与劳动者约定合同终止时间的劳动合同。**
用人单位与劳动者协商一致,可以订立固定期限劳动合同。
**第十四条 无固定期限劳动合同,是指用人单位与劳动者约定无确定终止时间的劳动合同。**
用人单位与劳动者协商一致,可以订立无固定期限劳动合同。有下列情形之一,劳动者提出或者同意续订、订立劳动合同的,除劳动者提出订立固定期限劳动合同外,应当订立无固定期限劳动合同:
(一)劳动者在该用人单位连续工作满十年的;
(二)用人单位初次实行劳动合同制度或者国有企业改制重新订立劳动合同时,劳动者在该用人单位连续工作满十年且距法定退休年龄不足十年的;
(三)连续订立二次固定期限劳动合同,且劳动者没有本法第三十九条和第四十条第一项、第二项规定的情形,续订劳动合同的。
用人单位自用工之日起满一年不与劳动者订立书面劳动合同的,视为用人单位与劳动者已订立无固定期限劳动合同。
**第十七条 劳动合同应当具备以下条款:**
(一)用人单位的名称、住所和法定代表人或者主要负责人;
(二)劳动者的姓名、住址和居民身份证或者其他有效身份证件号码;
(三)劳动合同期限;
(四)工作内容和工作地点;
(五)工作时间和休息休假;
(六)劳动报酬;
(七)社会保险;
(八)劳动保护、劳动条件和职业危害防护;
(九)法律、法规规定应当纳入劳动合同的其他事项。
劳动合同除前款规定的必备条款外,用人单位与劳动者可以约定试用期、培训、保守秘密、补充保险和福利待遇等其他事项。
**第十九条 劳动合同期限三个月以上不满一年的,试用期不得超过一个月;劳动合同期限一年以上不满三年的,试用期不得超过二个月;三年以上固定期限和无固定期限的劳动合同,试用期不得超过六个月。**
同一用人单位与同一劳动者只能约定一次试用期。
以完成一定工作任务为期限的劳动合同或者劳动合同期限不满三个月的,不得约定试用期。
试用期包含在劳动合同期限内。劳动合同仅约定试用期的,试用期不成立,该期限为劳动合同期限。
**第二十三条 用人单位与劳动者可以在劳动合同中约定保守用人单位的商业秘密和与知识产权相关的保密事项。**
对负有保密义务的劳动者,用人单位可以在劳动合同或者保密协议中与劳动者约定竞业限制条款,并约定在解除或者终止劳动合同后,在竞业限制期限内按月给予劳动者经济补偿。劳动者违反竞业限制约定的,应当按照约定向用人单位支付违约金。
**第二十四条 竞业限制的人员限于用人单位的高级管理人员、高级技术人员和其他负有保密义务的人员。竞业限制的范围、地域、期限由用人单位与劳动者约定,竞业限制的约定不得违反法律、法规的规定。**
在解除或者终止劳动合同后,前款规定的人员到与本单位生产或者经营同类产品、从事同类业务的有竞争关系的其他用人单位,或者自己开业生产或者经营同类产品、从事同类业务的竞业限制期限,不得超过二年。
**第二十五条 除本法第二十二条和第二十三条规定的情形外,用人单位不得与劳动者约定由劳动者承担违约金。**
**第二十六条 下列劳动合同无效或者部分无效:**
(一)以欺诈、胁迫的手段或者乘人之危,使对方在违背真实意思的情况下订立或者变更劳动合同的;
(二)用人单位免除自己的法定责任、排除劳动者权利的;
(三)违反法律、行政法规强制性规定的。
对劳动合同的无效或者部分无效有争议的,由劳动争议仲裁机构或者人民法院确认。
**第三十条 用人单位应当按照劳动合同约定和国家规定,向劳动者及时足额支付劳动报酬。**
用人单位拖欠或者未足额支付劳动报酬的,劳动者可以依法向当地人民法院申请支付令,人民法院应当依法发出支付令。
**第三十一条 用人单位应当严格执行劳动定额标准,不得强迫或者变相强迫劳动者加班。用人单位安排加班的,应当按照国家有关规定向劳动者支付加班费。**
**第三十五条 用人单位与劳动者协商一致,可以变更劳动合同约定的内容。变更劳动合同,应当采用书面形式。**
变更后的劳动合同文本由用人单位和劳动者各执一份。
**第三十六条 用人单位与劳动者协商一致,可以解除劳动合同。**
**第三十七条 劳动者提前三十日以书面形式通知用人单位,可以解除劳动合同。劳动者在试用期内提前三日通知用人单位,可以解除劳动合同。**
**第三十八条 用人单位有下列情形之一的,劳动者可以解除劳动合同:**
(一)未按照劳动合同约定提供劳动保护或者劳动条件的;
(二)未及时足额支付劳动报酬的;
(三)未依法为劳动者缴纳社会保险费的;
(四)用人单位的规章制度违反法律、法规的规定,损害劳动者权益的;
(五)因本法第二十六条第一款规定的情形致使劳动合同无效的;
(六)法律、行政法规规定劳动者可以解除劳动合同的其他情形。
用人单位以暴力、威胁或者非法限制人身自由的手段强迫劳动者劳动的,或者用人单位违章指挥、强令冒险作业危及劳动者人身安全的,劳动者可以立即解除劳动合同,不需事先告知用人单位。
**第三十九条 劳动者有下列情形之一的,用人单位可以解除劳动合同:**
(一)在试用期间被证明不符合录用条件的;
(二)严重违反用人单位的规章制度的;
(三)严重失职,营私舞弊,给用人单位造成重大损害的;
(四)劳动者同时与其他用人单位建立劳动关系,对完成本单位的工作任务造成严重影响,或者经用人单位提出,拒不改正的;
(五)因本法第二十六条第一款第一项规定的情形致使劳动合同无效的;
(六)被依法追究刑事责任的。
**第四十条 有下列情形之一的,用人单位提前三十日以书面形式通知劳动者本人或者额外支付劳动者一个月工资后,可以解除劳动合同:**
(一)劳动者患病或者非因工负伤,在规定的医疗期满后不能从事原工作,也不能从事由用人单位另行安排的工作的;
(二)劳动者不能胜任工作,经过培训或者调整工作岗位,仍不能胜任工作的;
(三)劳动合同订立时所依据的客观情况发生重大变化,致使劳动合同无法履行,经用人单位与劳动者协商,未能就变更劳动合同内容达成协议的。
**第四十一条 有下列情形之一,需要裁减人员二十人以上或者裁减不足二十人但占企业职工总数百分之十以上的,用人单位提前三十日向工会或者全体职工说明情况,听取工会或者职工的意见后,裁减人员方案经向劳动行政部门报告,可以裁减人员:**
(一)依照企业破产法规定进行重整的;
(二)生产经营发生严重困难的;
(三)企业转产、重大技术革新或者经营方式调整,经变更劳动合同后,仍需裁减人员的;
(四)其他因劳动合同订立时所依据的客观经济情况发生重大变化,致使劳动合同无法履行的。
裁减人员时,应当优先留用下列人员:
(一)与本单位订立较长期限的固定期限劳动合同的;
(二)与本单位订立无固定期限劳动合同的;
(三)家庭无其他就业人员,有需要扶养的老人或者未成年人的。
用人单位依照本条第一款规定裁减人员,在六个月内重新招用人员的,应当通知被裁减的人员,并在同等条件下优先招用被裁减的人员。
**第四十二条 劳动者有下列情形之一的,用人单位不得依照本法第四十条、第四十一条的规定解除劳动合同:**
(一)从事接触职业病危害作业的劳动者未进行离岗前职业健康检查,或者疑似职业病病人在诊断或者医学观察期间的;
(二)在本单位患职业病或者因工负伤并被确认丧失或者部分丧失劳动能力的;
(三)患病或者非因工负伤,在规定的医疗期内的;
(四)女职工在孕期、产期、哺乳期的;
(五)在本单位连续工作满十五年,且距法定退休年龄不足五年的;
(六)法律、行政法规规定的其他情形。
**第四十四条 有下列情形之一的,劳动合同终止:**
(一)劳动合同期满的;
(二)劳动者开始依法享受基本养老保险待遇的;
(三)劳动者死亡,或者被人民法院宣告死亡或者宣告失踪的;
(四)用人单位被依法宣告破产的;
(五)用人单位被吊销营业执照、责令关闭、撤销或者用人单位决定提前解散的;
(六)法律、行政法规规定的其他情形。
**第四十六条 有下列情形之一的,用人单位应当向劳动者支付经济补偿:**
(一)劳动者依照本法第三十八条规定解除劳动合同的;
(二)用人单位依照本法第三十六条规定向劳动者提出解除劳动合同并与劳动者协商一致解除劳动合同的;
(三)用人单位依照本法第四十条规定解除劳动合同的;
(四)用人单位依照本法第四十一条第一款规定解除劳动合同的;
(五)除用人单位维持或者提高劳动合同约定条件续订劳动合同,劳动者不同意续订的情形外,依照本法第四十四条第一项规定终止固定期限劳动合同的;
(六)依照本法第四十四条第四项、第五项规定终止劳动合同的;
(七)法律、行政法规规定的其他情形。
**第四十七条 经济补偿按劳动者在本单位工作的年限,每满一年支付一个月工资的标准向劳动者支付。六个月以上不满一年的,按一年计算;不满六个月的,向劳动者支付半个月工资的经济补偿。**
劳动者月工资高于用人单位所在直辖市、设区的市级人民政府公布的本地区上年度职工月平均工资三倍的,向其支付经济补偿的标准按职工月平均工资三倍的数额支付,向其支付经济补偿的年限最高不超过十二年。
本条所称月工资是指劳动者在劳动合同解除或者终止前十二个月的平均工资。
**第四十八条 用人单位违反本法规定解除或者终止劳动合同,劳动者要求继续履行劳动合同的,用人单位应当继续履行;劳动者不要求继续履行劳动合同或者劳动合同已经不能继续履行的,用人单位应当依照本法第八十七条规定支付赔偿金。**
**第五十条 用人单位应当在解除或者终止劳动合同时出具解除或者终止劳动合同的证明,并在十五日内为劳动者办理档案和社会保险关系转移手续。**
劳动者应当按照双方约定,办理工作交接。用人单位依照本法有关规定应当向劳动者支付经济补偿的,在办结工作交接时支付。
用人单位对已经解除或者终止的劳动合同的文本,至少保存二年备查。
第一节 集体合同
**第五十八条 劳务派遣单位是本法所称用人单位,应当履行用人单位对劳动者的义务。劳务派遣单位与被派遣劳动者订立的劳动合同,除应当载明本法第十七条规定的事项外,还应当载明被派遣劳动者的用工单位以及派遣期限、工作岗位等情况。**
劳务派遣单位应当与被派遣劳动者订立二年以上的固定期限劳动合同,按月支付劳动报酬;被派遣劳动者在无工作期间,劳务派遣单位应当按照所在地人民政府规定的最低工资标准,向其按月支付报酬。
**第六十二条 用工单位应当履行下列义务:**
(一)执行国家劳动标准,提供相应的劳动条件和劳动保护;
(二)告知被派遣劳动者的工作要求和劳动报酬;
(三)支付加班费、绩效奖金,提供与工作岗位相关的福利待遇;
(四)对在岗被派遣劳动者进行工作岗位所必需的培训;
(五)连续用工的,实行正常的工资调整机制。
用工单位不得将被派遣劳动者再派遣到其他用人单位。
**第六十三条 被派遣劳动者享有与用工单位的劳动者同工同酬的权利。用工单位应当按照同工同酬原则,对被派遣劳动者与本单位同类岗位的劳动者实行相同的劳动报酬分配办法。用工单位无同类岗位劳动者的,参照用工单位所在地相同或者相近岗位劳动者的劳动报酬确定。**
劳务派遣单位与被派遣劳动者订立的劳动合同和与用工单位订立的劳务派遣协议,载明或者约定的向被派遣劳动者支付的劳动报酬应当符合前款规定。
**第六十六条 劳动合同用工是我国的企业基本用工形式。劳务派遣用工是补充形式,只能在临时性、辅助性或者替代性的工作岗位上实施。**
前款规定的临时性工作岗位是指存续时间不超过六个月的岗位;辅助性工作岗位是指为主营业务岗位提供服务的非主营业务岗位;替代性工作岗位是指用工单位的劳动者因脱产学习、休假等原因无法工作的一定期间内,可以由其他劳动者替代工作的岗位。
用工单位应当严格控制劳务派遣用工数量,不得超过其用工总量的一定比例,具体比例由国务院劳动行政部门规定。
**第八十二条 用人单位自用工之日起超过一个月不满一年未与劳动者订立书面劳动合同的,应当向劳动者每月支付二倍的工资。**
用人单位违反本法规定不与劳动者订立无固定期限劳动合同的,自应当订立无固定期限劳动合同之日起向劳动者每月支付二倍的工资。
**第八十五条 用人单位有下列情形之一的,由劳动行政部门责令限期支付劳动报酬、加班费或者经济补偿;劳动报酬低于当地最低工资标准的,应当支付其差额部分;逾期不支付的,责令用人单位按应付金额百分之五十以上百分之一百以下的标准向劳动者加付赔偿金:**
(一)未按照劳动合同的约定或者国家规定及时足额支付劳动者劳动报酬的;
(二)低于当地最低工资标准支付劳动者工资的;
(三)安排加班不支付加班费的;
(四)解除或者终止劳动合同,未依照本法规定向劳动者支付经济补偿的。
**第八十七条 用人单位违反本法规定解除或者终止劳动合同的,应当依照本法第四十七条规定的经济补偿标准的二倍向劳动者支付赔偿金。**
FILE:references/simplified/劳动合同法实施条例.md
# 中华人民共和国劳动合同法实施条例(高频版)
**第四条 劳动合同法规定的用人单位设立的分支机构,依法取得营业执照或者登记证书的,可以作为用人单位与劳动者订立劳动合同;未依法取得营业执照或者登记证书的,受用人单位委托可以与劳动者订立劳动合同。**
**第五条 自用工之日起一个月内,经用人单位书面通知后,劳动者不与用人单位订立书面劳动合同的,用人单位应当书面通知劳动者终止劳动关系,无需向劳动者支付经济补偿,但是应当依法向劳动者支付其实际工作时间的劳动报酬。**
**第六条 用人单位自用工之日起超过一个月不满一年未与劳动者订立书面劳动合同的,应当依照劳动合同法第八十二条的规定向劳动者每月支付两倍的工资,并与劳动者补订书面劳动合同;劳动者不与用人单位订立书面劳动合同的,用人单位应当书面通知劳动者终止劳动关系,并依照劳动合同法第四十七条的规定支付经济补偿。**
前款规定的用人单位向劳动者每月支付两倍工资的起算时间为用工之日起满一个月的次日,截止时间为补订书面劳动合同的前一日。
**第七条 用人单位自用工之日起满一年未与劳动者订立书面劳动合同的,自用工之日起满一个月的次日至满一年的前一日应当依照劳动合同法第八十二条的规定向劳动者每月支付两倍的工资,并视为自用工之日起满一年的当日已经与劳动者订立无固定期限劳动合同,应当立即与劳动者补订书面劳动合同。**
**第十条 劳动者非因本人原因从原用人单位被安排到新用人单位工作的,劳动者在原用人单位的工作年限合并计算为新用人单位的工作年限。原用人单位已经向劳动者支付经济补偿的,新用人单位在依法解除、终止劳动合同计算支付经济补偿的工作年限时,不再计算劳动者在原用人单位的工作年限。**
**第十一条 除劳动者与用人单位协商一致的情形外,劳动者依照劳动合同法第十四条第二款的规定,提出订立无固定期限劳动合同的,用人单位应当与其订立无固定期限劳动合同。对劳动合同的内容,双方应当按照合法、公平、平等自愿、协商一致、诚实信用的原则协商确定;对协商不一致的内容,依照劳动合同法第十八条的规定执行。**
**第十三条 用人单位与劳动者不得在劳动合同法第四十四条规定的劳动合同终止情形之外约定其他的劳动合同终止条件。**
**第十四条 劳动合同履行地与用人单位注册地不一致的,有关劳动者的最低工资标准、劳动保护、劳动条件、职业危害防护和本地区上年度职工月平均工资标准等事项,按照劳动合同履行地的有关规定执行;用人单位注册地的有关标准高于劳动合同履行地的有关标准,且用人单位与劳动者约定按照用人单位注册地的有关规定执行的,从其约定。**
**第十五条 劳动者在试用期的工资不得低于本单位相同岗位最低档工资的80%或者不得低于劳动合同约定工资的80%,并不得低于用人单位所在地的最低工资标准。**
**第十六条 劳动合同法第二十二条第二款规定的培训费用,包括用人单位为了对劳动者进行专业技术培训而支付的有凭证的培训费用、培训期间的差旅费用以及因培训产生的用于该劳动者的其他直接费用。**
**第十七条 劳动合同期满,但是用人单位与劳动者依照劳动合同法第二十二条的规定约定的服务期尚未到期的,劳动合同应当续延至服务期满;双方另有约定的,从其约定。**
**第十八条 有下列情形之一的,依照劳动合同法规定的条件、程序,劳动者可以与用人单位解除固定期限劳动合同、无固定期限劳动合同或者以完成一定工作任务为期限的劳动合同:**
(一)劳动者与用人单位协商一致的;
(二)劳动者提前30日以书面形式通知用人单位的;
(三)劳动者在试用期内提前3日通知用人单位的;
(四)用人单位未按照劳动合同约定提供劳动保护或者劳动条件的;
(五)用人单位未及时足额支付劳动报酬的;
(六)用人单位未依法为劳动者缴纳社会保险费的;
(七)用人单位的规章制度违反法律、法规的规定,损害劳动者权益的;
(八)用人单位以欺诈、胁迫的手段或者乘人之危,使劳动者在违背真实意思的情况下订立或者变更劳动合同的;
(九)用人单位在劳动合同中免除自己的法定责任、排除劳动者权利的;
(十)用人单位违反法律、行政法规强制性规定的;
(十一)用人单位以暴力、威胁或者非法限制人身自由的手段强迫劳动者劳动的;
(十二)用人单位违章指挥、强令冒险作业危及劳动者人身安全的;
(十三)法律、行政法规规定劳动者可以解除劳动合同的其他情形。
**第十九条 有下列情形之一的,依照劳动合同法规定的条件、程序,用人单位可以与劳动者解除固定期限劳动合同、无固定期限劳动合同或者以完成一定工作任务为期限的劳动合同:**
(一)用人单位与劳动者协商一致的;
(二)劳动者在试用期间被证明不符合录用条件的;
(三)劳动者严重违反用人单位的规章制度的;
(四)劳动者严重失职,营私舞弊,给用人单位造成重大损害的;
(五)劳动者同时与其他用人单位建立劳动关系,对完成本单位的工作任务造成严重影响,或者经用人单位提出,拒不改正的;
(六)劳动者以欺诈、胁迫的手段或者乘人之危,使用人单位在违背真实意思的情况下订立或者变更劳动合同的;
(七)劳动者被依法追究刑事责任的;
(八)劳动者患病或者非因工负伤,在规定的医疗期满后不能从事原工作,也不能从事由用人单位另行安排的工作的;
(九)劳动者不能胜任工作,经过培训或者调整工作岗位,仍不能胜任工作的;
(十)劳动合同订立时所依据的客观情况发生重大变化,致使劳动合同无法履行,经用人单位与劳动者协商,未能就变更劳动合同内容达成协议的;
(十一)用人单位依照企业破产法规定进行重整的;
(十二)用人单位生产经营发生严重困难的;
(十三)企业转产、重大技术革新或者经营方式调整,经变更劳动合同后,仍需裁减人员的;
(十四)其他因劳动合同订立时所依据的客观经济情况发生重大变化,致使劳动合同无法履行的。
**第二十条 用人单位依照劳动合同法第四十条的规定,选择额外支付劳动者一个月工资解除劳动合同的,其额外支付的工资应当按照该劳动者上一个月的工资标准确定。**
**第二十二条 以完成一定工作任务为期限的劳动合同因任务完成而终止的,用人单位应当依照劳动合同法第四十七条的规定向劳动者支付经济补偿。**
**第二十三条 用人单位依法终止工伤职工的劳动合同的,除依照劳动合同法第四十七条的规定支付经济补偿外,还应当依照国家有关工伤保险的规定支付一次性工伤医疗补助金和伤残就业补助金。**
**第二十五条 用人单位违反劳动合同法的规定解除或者终止劳动合同,依照劳动合同法第八十七条的规定支付了赔偿金的,不再支付经济补偿。赔偿金的计算年限自用工之日起计算。**
**第二十六条 用人单位与劳动者约定了服务期,劳动者依照劳动合同法第三十八条的规定解除劳动合同的,不属于违反服务期的约定,用人单位不得要求劳动者支付违约金。**
有下列情形之一,用人单位与劳动者解除约定服务期的劳动合同的,劳动者应当按照劳动合同的约定向用人单位支付违约金:
(一)劳动者严重违反用人单位的规章制度的;
(二)劳动者严重失职,营私舞弊,给用人单位造成重大损害的;
(三)劳动者同时与其他用人单位建立劳动关系,对完成本单位的工作任务造成严重影响,或者经用人单位提出,拒不改正的;
(四)劳动者以欺诈、胁迫的手段或者乘人之危,使用人单位在违背真实意思的情况下订立或者变更劳动合同的;
(五)劳动者被依法追究刑事责任的。
**第二十七条 劳动合同法第四十七条规定的经济补偿的月工资按照劳动者应得工资计算,包括计时工资或者计件工资以及奖金、津贴和补贴等货币性收入。劳动者在劳动合同解除或者终止前12个月的平均工资低于当地最低工资标准的,按照当地最低工资标准计算。劳动者工作不满12个月的,按照实际工作的月数计算平均工资。**
**第二十八条 用人单位或者其所属单位出资或者合伙设立的劳务派遣单位,向本单位或者所属单位派遣劳动者的,属于劳动合同法第六十七条规定的不得设立的劳务派遣单位。**
**第二十九条 用工单位应当履行劳动合同法第六十二条规定的义务,维护被派遣劳动者的合法权益。**
**第三十条 劳务派遣单位不得以非全日制用工形式招用被派遣劳动者。**
**第三十一条 劳务派遣单位或者被派遣劳动者依法解除、终止劳动合同的经济补偿,依照劳动合同法第四十六条、第四十七条的规定执行。**
**第三十二条 劳务派遣单位违法解除或者终止被派遣劳动者的劳动合同的,依照劳动合同法第四十八条的规定执行。**
**第三十三条 用人单位违反劳动合同法有关建立职工名册规定的,由劳动行政部门责令限期改正;逾期不改正的,由劳动行政部门处2000元以上2万元以下的罚款。**
**第三十四条 用人单位依照劳动合同法的规定应当向劳动者每月支付两倍的工资或者应当向劳动者支付赔偿金而未支付的,劳动行政部门应当责令用人单位支付。**
**第三十五条 用工单位违反劳动合同法和本条例有关劳务派遣规定的,由劳动行政部门和其他有关主管部门责令改正;情节严重的,以每位被派遣劳动者1000元以上5000元以下的标准处以罚款;给被派遣劳动者造成损害的,劳务派遣单位和用工单位承担连带赔偿责任。**
**第三十七条 劳动者与用人单位因订立、履行、变更、解除或者终止劳动合同发生争议的,依照《中华人民共和国劳动争议调解仲裁法》的规定处理。**
FILE:references/simplified/司法解释(一).md
# 最高人民法院关于审理劳动争议案件适用法律问题的解释(一)(高频版)
**第一条 劳动者与用人单位之间发生的下列纠纷,属于劳动争议,当事人不服劳动争议仲裁机构作出的裁决,依法提起诉讼的,人民法院应予受理:**
(一)劳动者与用人单位在履行劳动合同过程中发生的纠纷;
(二)劳动者与用人单位之间没有订立书面劳动合同,但已形成劳动关系后发生的纠纷;
(三)劳动者与用人单位因劳动关系是否已经解除或者终止,以及应否支付解除或者终止劳动关系经济补偿金发生的纠纷;
(四)劳动者与用人单位解除或者终止劳动关系后,请求用人单位返还其收取的劳动合同定金、保证金、抵押金、抵押物发生的纠纷,或者办理劳动者的人事档案、社会保险关系等移转手续发生的纠纷;
(五)劳动者以用人单位未为其办理社会保险手续,且社会保险经办机构不能补办导致其无法享受社会保险待遇为由,要求用人单位赔偿损失发生的纠纷;
(六)劳动者退休后,与尚未参加社会保险统筹的原用人单位因追索养老金、医疗费、工伤保险待遇和其他社会保险待遇而发生的纠纷;
(七)劳动者因为工伤、职业病,请求用人单位依法给予工伤保险待遇发生的纠纷;
(八)劳动者依据劳动合同法第八十五条规定,要求用人单位支付加付赔偿金发生的纠纷;
(九)因企业自主进行改制发生的纠纷。
**第三条 劳动争议案件由用人单位所在地或者劳动合同履行地的基层人民法院管辖。**
劳动合同履行地不明确的,由用人单位所在地的基层人民法院管辖。
法律另有规定的,依照其规定。
**第十二条 劳动争议仲裁机构逾期未作出受理决定或仲裁裁决,当事人直接提起诉讼的,人民法院应予受理,但申请仲裁的案件存在下列事由的除外:**
(一)移送管辖的;
(二)正在送达或者送达延误的;
(三)等待另案诉讼结果、评残结论的;
(四)正在等待劳动争议仲裁机构开庭的;
(五)启动鉴定程序或者委托其他部门调查取证的;
(六)其他正当事由。
当事人以劳动争议仲裁机构逾期未作出仲裁裁决为由提起诉讼的,应当提交该仲裁机构出具的受理通知书或者其他已接受仲裁申请的凭证、证明。
**第十三条 劳动者依据劳动合同法第三十条第二款和调解仲裁法第十六条规定向人民法院申请支付令,符合民事诉讼法第十七章督促程序规定的,人民法院应予受理。**
依据劳动合同法第三十条第二款规定申请支付令被人民法院裁定终结督促程序后,劳动者就劳动争议事项直接提起诉讼的,人民法院应当告知其先向劳动争议仲裁机构申请仲裁。
依据调解仲裁法第十六条规定申请支付令被人民法院裁定终结督促程序后,劳动者依据调解协议直接提起诉讼的,人民法院应予受理。
**第十四条 人民法院受理劳动争议案件后,当事人增加诉讼请求的,如该诉讼请求与讼争的劳动争议具有不可分性,应当合并审理;如属独立的劳动争议,应当告知当事人向劳动争议仲裁机构申请仲裁。**
**第十五条 劳动者以用人单位的工资欠条为证据直接提起诉讼,诉讼请求不涉及劳动关系其他争议的,视为拖欠劳动报酬争议,人民法院按照普通民事纠纷受理。**
**第二十条 劳动争议仲裁机构作出的同一仲裁裁决同时包含终局裁决事项和非终局裁决事项,当事人不服该仲裁裁决向人民法院提起诉讼的,应当按照非终局裁决处理。**
**第二十四条 当事人申请人民法院执行劳动争议仲裁机构作出的发生法律效力的裁决书、调解书,被申请人提出证据证明劳动争议仲裁裁决书、调解书有下列情形之一,并经审查核实的,人民法院可以根据民事诉讼法第二百三十七条规定,裁定不予执行:**
(一)裁决的事项不属于劳动争议仲裁范围,或者劳动争议仲裁机构无权仲裁的;
(二)适用法律、法规确有错误的;
(三)违反法定程序的;
(四)裁决所根据的证据是伪造的;
(五)对方当事人隐瞒了足以影响公正裁决的证据的;
(六)仲裁员在仲裁该案时有索贿受贿、徇私舞弊、枉法裁决行为的;
(七)人民法院认定执行该劳动争议仲裁裁决违背社会公共利益的。
人民法院在不予执行的裁定书中,应当告知当事人在收到裁定书之次日起三十日内,可以就该劳动争议事项向人民法院提起诉讼。
**第二十五条 劳动争议仲裁机构作出终局裁决,劳动者向人民法院申请执行,用人单位向劳动争议仲裁机构所在地的中级人民法院申请撤销的,人民法院应当裁定中止执行。**
用人单位撤回撤销终局裁决申请或者其申请被驳回的,人民法院应当裁定恢复执行。仲裁裁决被撤销的,人民法院应当裁定终结执行。
用人单位向人民法院申请撤销仲裁裁决被驳回后,又在执行程序中以相同理由提出不予执行抗辩的,人民法院不予支持。
**第二十六条 用人单位与其它单位合并的,合并前发生的劳动争议,由合并后的单位为当事人;用人单位分立为若干单位的,其分立前发生的劳动争议,由分立后的实际用人单位为当事人。**
用人单位分立为若干单位后,具体承受劳动权利义务的单位不明确的,分立后的单位均为当事人。
FILE:references/simplified/司法解释(二).md
# 最高人民法院关于审理劳动争议案件适用法律问题的解释(二)(高频版)
**第六条 用人单位未依法与劳动者订立书面劳动合同,应当支付劳动者的二倍工资按月计算;不满一个月的,按该月实际工作日计算。**
**第七条 劳动者以用人单位未订立书面劳动合同为由,请求用人单位支付二倍工资的,人民法院依法予以支持,但用人单位举证证明存在下列情形之一的除外:**
(一)因不可抗力导致未订立的;
(二)因劳动者本人故意或者重大过失未订立的;
(三)法律、行政法规规定的其他情形。
**第八条 劳动合同期满,有下列情形之一的,人民法院认定劳动合同期限依法自动续延,不属于用人单位未订立书面劳动合同的情形:**
(一)劳动合同法第四十二条规定的用人单位不得解除劳动合同的;
(二)劳动合同法实施条例第十七条规定的服务期尚未到期的;
(三)工会法第十九条规定的任期未届满的。
**第九条 有证据证明存在劳动合同法第十四条第三款规定的“视为用人单位与劳动者已订立无固定期限劳动合同”情形,劳动者请求与用人单位订立书面劳动合同的,人民法院依法予以支持;劳动者以用人单位未及时补订书面劳动合同为由,请求用人单位支付视为已与劳动者订立无固定期限劳动合同期间二倍工资的,人民法院不予支持。**
**第十条 有下列情形之一的,人民法院应认定为符合劳动合同法第十四条第二款第三项“连续订立二次固定期限劳动合同”的规定:**
(一)用人单位与劳动者协商延长劳动合同期限累计达到一年以上,延长期限届满的;
(二)用人单位与劳动者约定劳动合同期满后自动续延,续延期限届满的;
(三)劳动者非因本人原因仍在原工作场所、工作岗位工作,用人单位变换劳动合同订立主体,但继续对劳动者进行劳动管理,合同期限届满的;
(四)以其他违反诚信原则的规避行为再次订立劳动合同,期限届满的。
**第十一条 劳动合同期满后,劳动者仍在用人单位工作,用人单位未表示异议超过一个月,劳动者请求用人单位以原条件续订劳动合同的,人民法院依法予以支持。**
符合订立无固定期限劳动合同情形,劳动者请求用人单位以原条件订立无固定期限劳动合同的,人民法院依法予以支持。
用人单位解除劳动合同,劳动者请求用人单位依法承担解除劳动合同法律后果的,人民法院依法予以支持。
**第十二条 除向劳动者支付正常劳动报酬外,用人单位与劳动者约定服务期限并提供特殊待遇,劳动者违反约定提前解除劳动合同且不符合劳动合同法第三十八条规定的单方解除劳动合同情形时,用人单位请求劳动者承担赔偿损失责任的,人民法院可以综合考虑实际损失、当事人的过错程度、已经履行的年限等因素确定劳动者应当承担的赔偿责任。**
**第十三条 劳动者未知悉、接触用人单位的商业秘密和与知识产权相关的保密事项,劳动者请求确认竞业限制条款不生效的,人民法院依法予以支持。**
竞业限制条款约定的竞业限制范围、地域、期限等内容与劳动者知悉、接触的商业秘密和与知识产权相关的保密事项不相适应,劳动者请求确认竞业限制条款超过合理比例部分无效的,人民法院依法予以支持。
**第十四条 用人单位与高级管理人员、高级技术人员和其他负有保密义务的人员约定在职期间竞业限制条款,劳动者以不得约定在职期间竞业限制、未支付经济补偿为由请求确认竞业限制条款无效的,人民法院不予支持。**
**第十五条 劳动者违反有效的竞业限制约定,用人单位请求劳动者按照约定返还已经支付的经济补偿并支付违约金的,人民法院依法予以支持。**
**第十六条 用人单位违法解除或者终止劳动合同后,有下列情形之一的,人民法院可以认定为劳动合同法第四十八条规定的“劳动合同已经不能继续履行”:**
(一)劳动合同在仲裁或者诉讼过程中期满且不存在应当依法续订、续延劳动合同情形的;
(二)劳动者开始依法享受基本养老保险待遇的;
(三)用人单位被宣告破产的;
(四)用人单位解散的,但因合并或者分立需要解散的除外;
(五)劳动者已经与其他用人单位建立劳动关系,对完成用人单位的工作任务造成严重影响,或者经用人单位提出,不与其他用人单位解除劳动合同的;
(六)存在劳动合同客观不能履行的其他情形的。
**第十七条 用人单位未按照国务院安全生产监督管理部门、卫生行政部门的规定组织从事接触职业病危害作业的劳动者进行离岗前的职业健康检查,劳动者在双方解除劳动合同后请求继续履行劳动合同的,人民法院依法予以支持,但有下列情形之一的除外:**
(一)一审法庭辩论终结前,用人单位已经组织劳动者进行职业健康检查且经检查劳动者未患职业病的;
(二)一审法庭辩论终结前,用人单位组织劳动者进行职业健康检查,劳动者无正当理由拒绝检查的。
**第十八条 用人单位违法解除、终止可以继续履行的劳动合同,劳动者请求用人单位支付违法解除、终止决定作出后至劳动合同继续履行前一日工资的,用人单位应当按照劳动者提供正常劳动时的工资标准向劳动者支付上述期间的工资。**
用人单位、劳动者对于劳动合同解除、终止都有过错的,应当各自承担相应的责任。
**第十九条 用人单位与劳动者约定或者劳动者向用人单位承诺无需缴纳社会保险费的,人民法院应当认定该约定或者承诺无效。用人单位未依法缴纳社会保险费,劳动者根据劳动合同法第三十八条第一款第三项规定请求解除劳动合同、由用人单位支付经济补偿的,人民法院依法予以支持。**
有前款规定情形,用人单位依法补缴社会保险费后,请求劳动者返还已支付的社会保险费补偿的,人民法院依法予以支持。
**第二十条 当事人在仲裁期间因自身原因未提出仲裁时效抗辩,在一审或者二审诉讼期间提出仲裁时效抗辩的,人民法院不予支持。当事人基于新的证据能够证明对方当事人请求权的仲裁时效期间届满的,人民法院应予支持。**
当事人未按照前款规定提出仲裁时效抗辩,以仲裁时效期间届满为由申请再审或者提出再审抗辩的,人民法院不予支持。
FILE:references/simplified/工伤保险条例.md
# 工伤保险条例(高频版)
**第二条 中华人民共和国境内的企业、事业单位、社会团体、民办非企业单位、基金会、律师事务所、会计师事务所等组织和有雇工的个体工商户(以下称用人单位)应当依照本条例规定参加工伤保险,为本单位全部职工或者雇工(以下称职工)缴纳工伤保险费。**
中华人民共和国境内的企业、事业单位、社会团体、民办非企业单位、基金会、律师事务所、会计师事务所等组织的职工和个体工商户的雇工,均有依照本条例的规定享受工伤保险待遇的权利。
**第七条 工伤保险基金由用人单位缴纳的工伤保险费、工伤保险基金的利息和依法纳入工伤保险基金的其他资金构成。**
**第八条 工伤保险费根据以支定收、收支平衡的原则,确定费率。**
国家根据不同行业的工伤风险程度确定行业的差别费率,并根据工伤保险费使用、工伤发生率等情况在每个行业内确定若干费率档次。行业差别费率及行业内费率档次由国务院社会保险行政部门制定,报国务院批准后公布施行。
统筹地区经办机构根据用人单位工伤保险费使用、工伤发生率等情况,适用所属行业内相应的费率档次确定单位缴费费率。
**第九条 国务院社会保险行政部门应当定期了解全国各统筹地区工伤保险基金收支情况,及时提出调整行业差别费率及行业内费率档次的方案,报国务院批准后公布施行。**
**第十条 用人单位应当按时缴纳工伤保险费。职工个人不缴纳工伤保险费。**
用人单位缴纳工伤保险费的数额为本单位职工工资总额乘以单位缴费费率之积。
对难以按照工资总额缴纳工伤保险费的行业,其缴纳工伤保险费的具体方式,由国务院社会保险行政部门规定。
**第十四条 职工有下列情形之一的,应当认定为工伤:**
(一)在工作时间和工作场所内,因工作原因受到事故伤害的;
(二)工作时间前后在工作场所内,从事与工作有关的预备性或者收尾性工作受到事故伤害的;
(三)在工作时间和工作场所内,因履行工作职责受到暴力等意外伤害的;
(四)患职业病的;
(五)因工外出期间,由于工作原因受到伤害或者发生事故下落不明的;
(六)在上下班途中,受到非本人主要责任的交通事故或者城市轨道交通、客运轮渡、火车事故伤害的;
(七)法律、行政法规规定应当认定为工伤的其他情形。
**第十五条 职工有下列情形之一的,视同工伤:**
(一)在工作时间和工作岗位,突发疾病死亡或者在48小时之内经抢救无效死亡的;
(二)在抢险救灾等维护国家利益、公共利益活动中受到伤害的;
(三)职工原在军队服役,因战、因公负伤致残,已取得革命伤残军人证,到用人单位后旧伤复发的。
职工有前款第(一)项、第(二)项情形的,按照本条例的有关规定享受工伤保险待遇;职工有前款第(三)项情形的,按照本条例的有关规定享受除一次性伤残补助金以外的工伤保险待遇。
**第十六条 职工符合本条例第十四条、第十五条的规定,但是有下列情形之一的,不得认定为工伤或者视同工伤:**
(一)故意犯罪的;
(二)醉酒或者吸毒的;
(三)自残或者自杀的。
**第十七条 职工发生事故伤害或者按照职业病防治法规定被诊断、鉴定为职业病,所在单位应当自事故伤害发生之日或者被诊断、鉴定为职业病之日起30日内,向统筹地区社会保险行政部门提出工伤认定申请。遇有特殊情况,经报社会保险行政部门同意,申请时限可以适当延长。**
用人单位未按前款规定提出工伤认定申请的,工伤职工或者其近亲属、工会组织在事故伤害发生之日或者被诊断、鉴定为职业病之日起1年内,可以直接向用人单位所在地统筹地区社会保险行政部门提出工伤认定申请。
按照本条第一款规定应当由省级社会保险行政部门进行工伤认定的事项,根据属地原则由用人单位所在地的设区的市级社会保险行政部门办理。
用人单位未在本条第一款规定的时限内提交工伤认定申请,在此期间发生符合本条例规定的工伤待遇等有关费用由该用人单位负担。
**第十八条 提出工伤认定申请应当提交下列材料:**
(一)工伤认定申请表;
(二)与用人单位存在劳动关系(包括事实劳动关系)的证明材料;
(三)医疗诊断证明或者职业病诊断证明书(或者职业病诊断鉴定书)。
工伤认定申请表应当包括事故发生的时间、地点、原因以及职工伤害程度等基本情况。
工伤认定申请人提供材料不完整的,社会保险行政部门应当一次性书面告知工伤认定申请人需要补正的全部材料。申请人按照书面告知要求补正材料后,社会保险行政部门应当受理。
**第二十条 社会保险行政部门应当自受理工伤认定申请之日起60日内作出工伤认定的决定,并书面通知申请工伤认定的职工或者其近亲属和该职工所在单位。**
社会保险行政部门对受理的事实清楚、权利义务明确的工伤认定申请,应当在15日内作出工伤认定的决定。
作出工伤认定决定需要以司法机关或者有关行政主管部门的结论为依据的,在司法机关或者有关行政主管部门尚未作出结论期间,作出工伤认定决定的时限中止。
社会保险行政部门工作人员与工伤认定申请人有利害关系的,应当回避。
**第二十一条 职工发生工伤,经治疗伤情相对稳定后存在残疾、影响劳动能力的,应当进行劳动能力鉴定。**
**第二十二条 劳动能力鉴定是指劳动功能障碍程度和生活自理障碍程度的等级鉴定。**
劳动功能障碍分为十个伤残等级,最重的为一级,最轻的为十级。
生活自理障碍分为三个等级:生活完全不能自理、生活大部分不能自理和生活部分不能自理。
劳动能力鉴定标准由国务院社会保险行政部门会同国务院卫生行政部门等部门制定。
**第二十三条 劳动能力鉴定由用人单位、工伤职工或者其近亲属向设区的市级劳动能力鉴定委员会提出申请,并提供工伤认定决定和职工工伤医疗的有关资料。**
**第三十条 职工因工作遭受事故伤害或者患职业病进行治疗,享受工伤医疗待遇。**
职工治疗工伤应当在签订服务协议的医疗机构就医,情况紧急时可以先到就近的医疗机构急救。
治疗工伤所需费用符合工伤保险诊疗项目目录、工伤保险药品目录、工伤保险住院服务标准的,从工伤保险基金支付。工伤保险诊疗项目目录、工伤保险药品目录、工伤保险住院服务标准,由国务院社会保险行政部门会同国务院卫生行政部门、食品药品监督管理部门等部门规定。
职工住院治疗工伤的伙食补助费,以及经医疗机构出具证明,报经办机构同意,工伤职工到统筹地区以外就医所需的交通、食宿费用从工伤保险基金支付,基金支付的具体标准由统筹地区人民政府规定。
工伤职工治疗非工伤引发的疾病,不享受工伤医疗待遇,按照基本医疗保险办法处理。
工伤职工到签订服务协议的医疗机构进行工伤康复的费用,符合规定的,从工伤保险基金支付。
**第三十一条 社会保险行政部门作出认定为工伤的决定后发生行政复议、行政诉讼的,行政复议和行政诉讼期间不停止支付工伤职工治疗工伤的医疗费用。**
**第三十二条 工伤职工因日常生活或者就业需要,经劳动能力鉴定委员会确认,可以安装假肢、矫形器、假眼、假牙和配置轮椅等辅助器具,所需费用按照国家规定的标准从工伤保险基金支付。**
**第三十三条 职工因工作遭受事故伤害或者患职业病需要暂停工作接受工伤医疗的,在停工留薪期内,原工资福利待遇不变,由所在单位按月支付。**
停工留薪期一般不超过12个月。伤情严重或者情况特殊,经设区的市级劳动能力鉴定委员会确认,可以适当延长,但延长不得超过12个月。工伤职工评定伤残等级后,停发原待遇,按照本章的有关规定享受伤残待遇。工伤职工在停工留薪期满后仍需治疗的,继续享受工伤医疗待遇。
生活不能自理的工伤职工在停工留薪期需要护理的,由所在单位负责。
**第三十七条 职工因工致残被鉴定为七级至十级伤残的,享受以下待遇:**
(一)从工伤保险基金按伤残等级支付一次性伤残补助金,标准为:七级伤残为13个月的本人工资,八级伤残为11个月的本人工资,九级伤残为9个月的本人工资,十级伤残为7个月的本人工资;
(二)劳动、聘用合同期满终止,或者职工本人提出解除劳动、聘用合同的,由工伤保险基金支付一次性工伤医疗补助金,由用人单位支付一次性伤残就业补助金。一次性工伤医疗补助金和一次性伤残就业补助金的具体标准由省、自治区、直辖市人民政府规定。
**第三十九条 职工因工死亡,其近亲属按照下列规定从工伤保险基金领取丧葬补助金、供养亲属抚恤金和一次性工亡补助金:**
(一)丧葬补助金为6个月的统筹地区上年度职工月平均工资;
(二)供养亲属抚恤金按照职工本人工资的一定比例发给由因工死亡职工生前提供主要生活来源、无劳动能力的亲属。标准为:配偶每月40%,其他亲属每人每月30%,孤寡老人或者孤儿每人每月在上述标准的基础上增加10%。核定的各供养亲属的抚恤金之和不应高于因工死亡职工生前的工资。供养亲属的具体范围由国务院社会保险行政部门规定;
(三)一次性工亡补助金标准为上一年度全国城镇居民人均可支配收入的20倍。
伤残职工在停工留薪期内因工伤导致死亡的,其近亲属享受本条第一款规定的待遇。
一级至四级伤残职工在停工留薪期满后死亡的,其近亲属可以享受本条第一款第(一)项、第(二)项规定的待遇。
**第四十条 伤残津贴、供养亲属抚恤金、生活护理费由统筹地区社会保险行政部门根据职工平均工资和生活费用变化等情况适时调整。调整办法由省、自治区、直辖市人民政府规定。**
**第四十一条 职工因工外出期间发生事故或者在抢险救灾中下落不明的,从事故发生当月起3个月内照发工资,从第4个月起停发工资,由工伤保险基金向其供养亲属按月支付供养亲属抚恤金。生活有困难的,可以预支一次性工亡补助金的50%。职工被人民法院宣告死亡的,按照本条例第三十九条职工因工死亡的规定处理。**
**第六十二条 用人单位依照本条例规定应当参加工伤保险而未参加的,由社会保险行政部门责令限期参加,补缴应当缴纳的工伤保险费,并自欠缴之日起,按日加收万分之五的滞纳金;逾期仍不缴纳的,处欠缴数额1倍以上3倍以下的罚款。**
依照本条例规定应当参加工伤保险而未参加工伤保险的用人单位职工发生工伤的,由该用人单位按照本条例规定的工伤保险待遇项目和标准支付费用。
用人单位参加工伤保险并补缴应当缴纳的工伤保险费、滞纳金后,由工伤保险基金和用人单位依照本条例的规定支付新发生的费用。
**第六十三条 用人单位违反本条例第十九条的规定,拒不协助社会保险行政部门对事故进行调查核实的,由社会保险行政部门责令改正,处2000元以上2万元以下的罚款。**
**第六十四条 本条例所称工资总额,是指用人单位直接支付给本单位全部职工的劳动报酬总额。**
本条例所称本人工资,是指工伤职工因工作遭受事故伤害或者患职业病前12个月平均月缴费工资。本人工资高于统筹地区职工平均工资300%的,按照统筹地区职工平均工资的300%计算;本人工资低于统筹地区职工平均工资60%的,按照统筹地区职工平均工资的60%计算。
FILE:_meta.json
{
"name": "labor-law-advisor",
"version": "1.0.0",
"description": "劳动法律咨询助手,内置《劳动合同法》《劳动争议调解仲裁法》等四部法律全文,支持条款引用、权益分析、维权路径指导和文书生成。",
"author": "Jeff",
"tags": ["labor-law", "legal", "employment", "dispute", "china"],
"references": [
"references/劳动合同法.md",
"references/劳动合同法实施条例.md",
"references/劳动争议调解仲裁法.md",
"references/工伤保险条例.md"
]
}
实时汇率换算专家。支持150+货币实时汇率、批量换算、多货币对比、历史汇率查询。零API Key,免费数据源。When user asks about currency exchange, conversion rates, USD to CNY, forex, or money conversion.
---
name: currency-converter-pro
description: 实时汇率换算专家。支持150+货币实时汇率、批量换算、多货币对比、历史汇率查询。零API Key,免费数据源。When user asks about currency exchange, conversion rates, USD to CNY, forex, or money conversion.
---
# Currency Converter Pro
**实时汇率换算专家** | Author: Lin Hui | Version 1.0.0 | MIT License
支持150+货币实时汇率查询、批量换算、历史汇率对比。零API Key,免费数据源。
## 核心功能
- ✅ 实时汇率查询(150+货币)
- ✅ 任意金额多货币换算
- ✅ 多货币横向对比(1000美元能换多少各货币)
- ✅ 历史汇率查询
- ✅ 零API Key,免费数据源
- ✅ 支持主要货币:CNY、USD、EUR、GBP、JPY、KRW、HKD、TWD、SGD、AUD、CAD、CHF、INR等
## 数据来源
- **open.er-api.com** — 免费汇率API,无需注册,无需Key
- 数据更新:每日多次自动更新
- 覆盖范围:150+全球主流货币
## 触发词
> "100美元换多少人民币" / "今日汇率" / "美元兑日元" / "1万港币值多少人民币" / "EUR to USD" / "exchange rate" / "汇率换算" / "人民币贬值了吗" / "1000块能换多少美元" / "历史汇率"
## 使用示例
### 货币换算
**输入:** 100 USD → CNY
**输出:**
```json
{
"from": "USD",
"to": "CNY",
"amount": 100,
"rate": 6.841274,
"result": 684.13,
"timestamp": "Mon, 27 Apr 2026",
"provider": "open.er-api.com"
}
```
### 多货币横向对比
**输入:** 1000 USD 换所有主流货币
**输出:**
```json
{
"from": "USD",
"amount": 1000,
"conversions": [
{"currency": "CNY", "amount": 6841.27},
{"currency": "EUR", "amount": 854.23},
{"currency": "GBP", "amount": 740.22},
{"currency": "JPY", "amount": 159540.6},
...
]
}
```
### 历史汇率
**输入:** 100 USD → CNY,2024-01-01
**输出:** 当天的美元兑人民币汇率(可用于对比汇率变化)
## 技术实现
```bash
# 单币种换算
python3 scripts/currency.py convert <金额> <源货币> <目标货币>
# 汇率列表
python3 scripts/currency.py rates <基准货币>
# 多货币横向对比
python3 scripts/currency.py top <金额> <源货币>
# 历史汇率
python3 scripts/currency.py historical <金额> <源货币> <目标货币> <日期YYYY-MM-DD>
```
## 支持货币(部分)
| 货币代码 | 名称 |
|---------|------|
| CNY | 人民币 |
| USD | 美元 |
| EUR | 欧元 |
| GBP | 英镑 |
| JPY | 日元 |
| KRW | 韩元 |
| HKD | 港币 |
| TWD | 新台币 |
| SGD | 新加坡元 |
| AUD | 澳元 |
| CAD | 加元 |
| CHF | 瑞士法郎 |
| INR | 印度卢比 |
| THB | 泰铢 |
| MYR | 林吉特 |
| PHP | 菲律宾比索 |
| VND | 越南盾 |
| IDR | 印尼盾 |
| AED | 阿联酋迪拉姆 |
| SAR | 沙特里亚尔 |
## 常见场景
| 场景 | 命令 |
|------|------|
| 海淘价格换算 | `convert 100 USD CNY` |
| 出国前准备 | `top 10000 CNY` |
| 汇率对比 | `rates USD` |
| 保值分析 | `historical 1000 USD CNY 2024-01-01` |
## 更新日志
### v1.0.0 (2026-04)
- 首发版本
- 150+货币实时汇率
- 零API Key,免费数据源
- 支持历史汇率查询
## ⚠️ Disclaimer
This tool is provided "as is" for informational purposes only. Data accuracy is not guaranteed. Not financial, legal, or professional advice. Always verify critical information from official sources.
本工具仅供信息参考,不保证数据完全准确,不构成任何金融/法律/专业建议。请以官方来源为准。
FILE:scripts/currency.py
#!/usr/bin/env python3
"""
Currency Converter Pro
Author: Lin Hui
Real-time exchange rates with multi-currency support.
Uses open.er-api.com (free, no API key required).
"""
import sys
import json
import subprocess
import urllib.request
import urllib.error
API_BASE = "https://open.er-api.com/v6"
def fetch_rates(base="USD"):
"""Fetch latest exchange rates from open.er-api.com"""
url = f"{API_BASE}/latest/{base}"
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read().decode())
if data.get("result") == "success":
return data
else:
return {"error": "API error: " + str(data)}
except urllib.error.URLError as e:
return {"error": "Network error: " + str(e)}
except Exception as e:
return {"error": str(e)}
def fetch_historical(base, date_str):
"""Fetch historical exchange rates (date format: YYYY-MM-DD)"""
url = f"{API_BASE}/historical/{date_str}?base={base}"
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode())
except Exception as e:
return {"error": str(e)}
def cmd_convert(args):
"""Convert amount from one currency to another"""
# args: [amount, from_currency, to_currency]
if len(args) < 3:
print(json.dumps({"error": "Usage: convert <amount> <from_currency> <to_currency>"}))
return
try:
amount = float(args[0])
from_curr = args[1].upper()
to_curr = args[2].upper()
except ValueError:
print(json.dumps({"error": "Invalid amount"}))
return
data = fetch_rates(from_curr)
if "error" in data:
print(json.dumps(data))
return
rates = data.get("rates", {})
if to_curr not in rates:
print(json.dumps({"error": f"Currency {to_curr} not found in rates"}))
return
rate = rates[to_curr]
converted = amount * rate
print(json.dumps({
"from": from_curr,
"to": to_curr,
"amount": amount,
"rate": round(rate, 6),
"result": round(converted, 2),
"timestamp": data.get("time_last_update_utc", ""),
"provider": "open.er-api.com"
}, ensure_ascii=False, indent=2))
def cmd_rates(args):
"""Show all rates for a base currency"""
base = args[0].upper() if args else "USD"
data = fetch_rates(base)
if "error" in data:
print(json.dumps(data))
return
rates = data.get("rates", {})
sorted_rates = dict(sorted(rates.items()))
# Format nicely
print(json.dumps({
"base": base,
"timestamp": data.get("time_last_update_utc", ""),
"rates": sorted_rates
}, ensure_ascii=False, indent=2))
def cmd_top(args):
"""Show top currencies by conversion value"""
if len(args) < 2:
print(json.dumps({"error": "Usage: top <amount> <from_currency>"}))
return
try:
amount = float(args[0])
from_curr = args[1].upper()
except ValueError:
print(json.dumps({"error": "Invalid amount"}))
return
data = fetch_rates(from_curr)
if "error" in data:
print(json.dumps(data))
return
rates = data.get("rates", {})
# Common currencies to show
common = ["CNY", "HKD", "TWD", "JPY", "KRW", "EUR", "GBP", "SGD", "AUD", "CAD",
"CHF", "JPY", "INR", "THB", "MYR", "PHP", "VND", "IDR", "AED", "SAR",
"USD"]
converted = []
for curr in common:
if curr in rates:
converted.append((curr, round(amount * rates[curr], 2)))
print(json.dumps({
"from": from_curr,
"amount": amount,
"conversions": [{"currency": c, "amount": a} for c, a in converted]
}, ensure_ascii=False, indent=2))
def cmd_historical(args):
"""Show historical rate between two currencies on a specific date"""
if len(args) < 3:
print(json.dumps({"error": "Usage: historical <amount> <from_currency> <to_currency> <date(YYYY-MM-DD)>"}))
return
try:
amount = float(args[0])
from_curr = args[1].upper()
to_curr = args[2].upper()
date_str = args[3] if len(args) > 3 else "2024-01-01"
except ValueError:
print(json.dumps({"error": "Invalid amount"}))
return
data = fetch_historical(from_curr, date_str)
if "error" in data:
print(json.dumps(data))
return
rates = data.get("rates", {})
if to_curr not in rates:
print(json.dumps({"error": f"Currency {to_curr} not found"}))
return
rate = rates[to_curr]
converted = amount * rate
print(json.dumps({
"from": from_curr,
"to": to_curr,
"amount": amount,
"date": date_str,
"rate": round(rate, 6),
"result": round(converted, 2),
"timestamp": data.get("time_last_update_utc", "")
}, ensure_ascii=False, indent=2))
def main():
if len(sys.argv) < 2:
print("Usage: currency.py <command> [args...]")
print("Commands: convert, rates, top, historical")
sys.exit(1)
cmd = sys.argv[1]
args = sys.argv[2:]
if cmd == "convert":
cmd_convert(args)
elif cmd == "rates":
cmd_rates(args)
elif cmd == "top":
cmd_top(args)
elif cmd == "historical":
cmd_historical(args)
else:
print(f"Unknown command: {cmd}")
if __name__ == "__main__":
main()
OpenClaw 个人 AgentOS 初始化向导 / Bootstrapper。Use when a user wants to initialize, diagnose, upgrade, repair, or health-check a new or existing OpenClaw setup; in...
---
name: openclaw-agent-onboarding
description: "OpenClaw 个人 AgentOS 初始化向导 / Bootstrapper。Use when a user wants to initialize, diagnose, upgrade, repair, or health-check a new or existing OpenClaw setup; install baseline skills; add web search and skill discovery; set up HOT/WARM/COLD memory; create an Obsidian-friendly Markdown knowledge base; configure Agent teams; establish self-evolution workflows; reduce context pollution; or bootstrap OpenClaw into a personal AgentOS. 触发词:OpenClaw 初始化、AgentOS 启动器、新用户引导、安装必要 skill、搭建三层记忆、个人知识库、Obsidian、Agent 团队、自进化、健康检查、一键修复、一键升级、上下文污染治理。"
version: "0.1.0"
author: "OpenClaw"
tags: [openclaw, onboarding, agentos, memory, skills, knowledge-base, obsidian, health-check]
---
# OpenClaw AgentOS Onboarding
This skill bootstraps a fresh or underpowered OpenClaw setup into a safe, maintainable personal AgentOS.
## Prime directive
Do not merely explain. Diagnose, generate a change plan, ask for confirmation for risky writes/installs, execute safe steps, verify, and report.
Default execution contract:
```text
Preflight → Plan → Confirm risky changes → Execute → Verify → Report → Leave rollback notes
```
## Safety rules
- Never delete user files.
- Never overwrite `AGENTS.md`, `MEMORY.md`, `SOUL.md`, `TOOLS.md`, `USER.md`, `DREAMS.md`.
- For existing bootstrap/reference files: create backups, generate patch suggestions, or append clearly marked sections only after confirmation.
- Do not install unknown-source skills automatically.
- Do not perform paid API/cloud actions without explicit confirmation.
- Do not write private user content into reusable public templates.
- Mark API-key-dependent skills before installing or configuring.
- Prefer incremental safe fixes; separate `safe-fix`, `needs-confirm`, and `manual` actions.
## Operating modes
Respond to either slash-style requests or natural language equivalents:
```text
/agentos bootstrap
/agentos diagnose
/agentos init
/agentos install
/agentos repair
/agentos upgrade
/agentos health
/agentos memory-check
/agentos skill-check
/agentos kb-init
/agentos kb-check
/agentos kb-obsidian
/agentos context-clean
/agentos report
```
Natural-language triggers include: “帮我初始化 OpenClaw”, “一键升级到 AgentOS”, “安装必要 skill”, “搭建三层记忆”, “搭建个人知识库”, “检查上下文污染”, “做健康检查”.
## Darwin optimization note
This skill was optimized with the Darwin rubric focus: concrete workflow, explicit boundaries, progressive disclosure, verification outputs, and common user prompts. Typical test prompts are stored in `test-prompts.json`.
## Decision matrix
Choose the narrowest mode that satisfies the user:
| User intent | Mode | References to read | Default action |
|---|---|---|---|
| “刚装 OpenClaw / 不知道怎么开始” | bootstrap | `skill-baseline.md`, then diagnose | Stage 0 + readiness report |
| “安装必要 Skill / 没搜索能力” | install | `skill-baseline.md`, `safety-policy.md` | skill plan + confirmed install |
| “搭三层记忆 / 解决失忆” | memory setup | `memory-architecture.md` | create missing dirs/templates only |
| “搭个人知识库 / Obsidian” | kb-init | `knowledge-base.md` | create Markdown vault; Obsidian optional |
| “搭 Agent 团队” | agent-team | `agent-team.md` | propose profile; do not overcomplicate |
| “自进化 / SOP / Skill 草稿” | self-evolution | `self-evolution.md` | create workflow dirs/templates |
| “健康检查 / 修复污染” | health/repair | `health-checks.md`, `context-hygiene.md` | diagnose + classify fixes |
| “安全/覆盖/安装来源” | safety review | `safety-policy.md` | block risky action until confirmed |
## Required output formats
### Change plan
```text
Goal:
Current level:
Target level:
Safe changes:
Needs confirmation:
Manual steps:
Files/directories affected:
Skills to install:
Verification commands/checks:
Rollback notes:
```
### Final report
```text
Current level → Target level:
Completed:
Skipped:
Installed/missing skills:
Created files/dirs:
Backups/patches:
Health score:
Risks/Pending confirmations:
Next 3 actions:
```
## Workflow
### 1. Diagnose first
Check:
```text
OpenClaw/gateway status, workspace path, skills dir, memory dir, docs dir,
AGENTS.md/MEMORY.md/SOUL.md/TOOLS.md/USER.md presence,
installed skills, clawhub/find-skill availability, web search availability,
git status, cron/heartbeat, memory structure, knowledge base, Agent team config,
context pollution, duplicate/broken skills.
```
Output maturity level:
```text
Level 0 fresh install
Level 1 basic assistant
Level 2 assistant with memory
Level 3 AgentOS with knowledge base/workflows
Level 4 multi-agent + self-evolution + health checks
Level 5 advanced personal AgentOS
```
### 2. Bootstrap Stage 0: skill discovery + web search
If the user lacks skill discovery or web search, fix this before advanced setup.
Minimum survival package:
```text
clawhub
find-skill
openclawmp
markdown
```
If `clawhub` is missing, recommend or run after confirmation:
```bash
npm i -g clawhub
```
Search/install examples:
```bash
clawhub search "web search"
clawhub install find-skill
clawhub install openclawmp
clawhub install markdown
```
If offline, generate directories/templates/manual install checklist and tell user to rerun install after network returns.
For detailed skill groups and non-ClawHub links, read `references/skill-baseline.md`.
### 3. Install baseline skill packages
Use packages from `references/skill-baseline.md`:
```text
bootstrap-minimal
bootstrap-search
bootstrap-docs
bootstrap-agentos-core
bootstrap-skill-lab
bootstrap-engineering
bootstrap-design
bootstrap-creator
```
Default standard order:
```text
1. bootstrap-minimal
2. bootstrap-search
3. bootstrap-docs
4. bootstrap-agentos-core
5. bootstrap-skill-lab
```
Always mark each skill as: installed / missing / failed / needs API key / non-ClawHub / manual.
### 4. Set up HOT/WARM/COLD memory
Create or propose:
```text
MEMORY.md # HOT, ≤150 lines recommended
memory/index.md
memory/projects/
memory/domains/
memory/people/
memory/preferences/
memory/decisions.md
memory/gotchas.md
memory/archive/
memory/logs/
memory/raw/
```
Rules: temporary info never goes into HOT; long content goes to WARM/COLD; conflicts are flagged, not overwritten. See `references/memory-architecture.md`.
### 5. Set up personal knowledge base / Obsidian-friendly vault
Create Markdown-first vault under `memory/wiki/`; Obsidian is optional.
Recommended structure:
```text
memory/wiki/00 Inbox/
memory/wiki/01 Projects/
memory/wiki/02 Areas/
memory/wiki/03 Resources/
memory/wiki/04 Concepts/
memory/wiki/05 People/
memory/wiki/06 Decisions/
memory/wiki/07 Workflows/
memory/wiki/08 Skills/
memory/wiki/09 Reviews/
memory/wiki/99 Archive/
```
Knowledge flow:
```text
Capture → Distill → Link → Operationalize → Archive
```
Tell users: without Obsidian, OpenClaw still works via Markdown; with Obsidian, open `memory/wiki/` as a vault to see graph/backlinks. See `references/knowledge-base.md`.
### 6. Configure Agent team
Offer three profiles:
```text
single: main-agent
three-agent: architect → executor → auditor
six-agent: pm → architect → reasoner → coder → auditor → monitor
```
Do not force multi-agent complexity on beginners. See `references/agent-team.md`.
### 7. Establish self-evolution workflows
Create or propose:
```text
memory/wiki/07 Workflows/TaskNotes/
memory/wiki/07 Workflows/SOP/
memory/wiki/07 Workflows/SkillDrafts/
memory/wiki/07 Workflows/ContextCaptures/
memory/wiki/07 Workflows/Checkpoints/
memory/wiki/07 Workflows/Evaluations/
memory/wiki/07 Workflows/SecurityIntake/
```
Flow:
```text
Task → TaskNote → SOP → SkillDraft → vetter → official Skill → index
```
See `references/self-evolution.md`.
### 8. Health checks and repair/upgrade
Health check dimensions:
```text
skills, memory, knowledge base, context hygiene, Agent team, cron/heartbeat,
OpenClaw service, logs, git state, security risks.
```
Output a score and separate P0/P1/P2 issues. For details, see `references/health-checks.md` and `references/context-hygiene.md`.
### 9. Verify
Before final report, verify what changed:
```text
- directories/files exist
- protected files were not overwritten
- installed skills contain SKILL.md
- diagnose script still emits valid JSON
- memory/wiki structure exists if requested
- health issues are classified as P0/P1/P2
```
If verification fails, report the blocker and propose repair; do not claim success.
### 10. Final report
Always finish with:
```text
current level, target level, completed changes, installed/missing skills,
created directories/files, backups, risks, pending confirmations, next steps.
```
## Implementation notes
- For simple advisory requests, do not execute writes; produce a plan.
- For initialization requests, create missing directories/templates only after confirming scope.
- If using scripts, read or run files in `scripts/` as needed. Scripts are helpers, not authority; safety rules above win.
FILE:results.tsv
skill phase score method notes
openclaw-agent-onboarding baseline 82 dry_run Strong concept and references; missing decision matrix, output formats, and explicit verification gate.
openclaw-agent-onboarding optimized 91 dry_run Added decision matrix, required change/final report formats, verification gate, and test prompts.
FILE:references/agent-team.md
# Agent Team Profiles
## Single-agent mode
```text
main-agent: main assistant/controller
```
Use for beginners and lightweight tasks.
## Three-agent mode
```text
architect → executor → auditor
```
- `architect`: decomposition, planning, architecture boundaries.
- `executor`: implementation, file operations, coding.
- `auditor`: review, tests, validation.
## Six-agent mode
```text
pm → architect → reasoner → coder → auditor → monitor
```
- `pm`: requirements, task board, acceptance criteria.
- `architect`: system design, boundaries, risk.
- `reasoner`: complex reasoning/root cause.
- `coder`: implementation.
- `auditor`: security/quality review.
- `monitor`: tests, logs, health, validation.
## Rules
- Do not force multi-agent mode for beginners.
- Planner does not write code.
- Executor does not change requirements unilaterally.
- Auditor does not replace executor.
- Monitor validates and reports, not scope-creeps.
FILE:references/memory-architecture.md
# HOT/WARM/COLD Memory Architecture
## HOT
File: `MEMORY.md`
Purpose: small, permanent, high-frequency context.
Recommended max: 150 lines.
Include:
```text
core rules, user preferences, current priority projects, safety boundaries, memory index links
```
Do not include:
```text
long logs, temporary tasks, raw docs, full reports, outdated rules
```
## WARM
Directory:
```text
memory/projects/
memory/domains/
memory/people/
memory/preferences/
```
Include project details, domain knowledge, people/contact context, preferences, durable background.
## COLD
Directory:
```text
memory/archive/
memory/logs/
memory/raw/
```
Include historical logs, raw material, old decisions, long reports, low-frequency references.
## Required structure
```text
memory/
├── index.md
├── projects/
├── domains/
├── people/
├── preferences/
├── decisions.md
├── gotchas.md
├── archive/
├── logs/
└── raw/
```
## Write policy
1. Decide if it is worth remembering.
2. Temporary info never enters HOT.
3. Long content goes WARM/COLD.
4. Conflicting memory is flagged, not overwritten.
5. Outdated rules move to archive.
6. Every durable memory includes date/source when possible.
7. Important WARM/COLD files are indexed in `memory/index.md`.
FILE:references/safety-policy.md
# Safety Policy
## Hard boundaries
- No deletion of user files.
- No overwrite of core bootstrap/persona/memory files.
- No paid actions without explicit confirmation.
- No unknown-source skill auto-install.
- No public publishing.
- No secrets in generated reports/templates.
## Protected files
```text
AGENTS.md
MEMORY.md
SOUL.md
TOOLS.md
USER.md
DREAMS.md
```
For protected files:
```text
read → diagnose → generate patch → show diff → confirm → backup → edit/append
```
## Install safety
- Prefer ClawHub or known GitHub sources.
- For non-ClawHub sources, show URL and require confirmation.
- Verify `SKILL.md` exists after install.
- Run vetter/health checks when available.
FILE:references/self-evolution.md
# Self-Evolution Workflow
## Directories
```text
memory/wiki/07 Workflows/TaskNotes/
memory/wiki/07 Workflows/SOP/
memory/wiki/07 Workflows/SkillDrafts/
memory/wiki/07 Workflows/ContextCaptures/
memory/wiki/07 Workflows/Checkpoints/
memory/wiki/07 Workflows/Evaluations/
memory/wiki/07 Workflows/SecurityIntake/
```
## Flow
```text
Task completion
→ Task Note
→ decide reuse value
→ SOP for repeatable workflows
→ SkillDraft for stable high-frequency SOP
→ skill-vetter review
→ install as formal skill
→ index in memory/index.md
```
## Rules
- Do not distill every task.
- Only stable/reusable/high-frequency flows become SOP/SkillDraft.
- Sensitive info must not enter public skill material.
- Dual-use/high-risk tools go to SecurityIntake, not automatic install.
- Failed lessons go to `gotchas.md` or project memory.
FILE:references/context-hygiene.md
# Context Hygiene
## Pollution symptoms
```text
MEMORY.md too long
conflicting rules
temporary tasks in HOT memory
old project details still active
duplicate skills
AGENTS.md contains long reports
raw logs in prompt-loaded files
```
## Rules
- HOT memory stays short.
- Long reports move to WARM/COLD.
- Old rules are archived, not left active.
- Project details stay in project files.
- Do not paste entire logs into always-loaded files.
- Use indexes and references instead of copying full content.
## Repair categories
```text
safe-fix: create missing dirs, add indexes, move obvious raw logs after backup
needs-confirm: modify core files, archive active rules, merge duplicates
manual: ambiguous conflicts, private identity/persona changes
```
FILE:references/health-checks.md
# Health Checks
## Dimensions
```text
Skill health
Memory health
Knowledge-base health
Context hygiene
Agent team config
cron/heartbeat
OpenClaw service status
logs/errors
workspace git state
security risks
```
## Suggested cadence
Daily:
```text
OpenClaw service status, failed tasks, P0 anomalies
```
Weekly:
```text
skill usage, memory pollution, Inbox cleanup, context bloat, Agent team config
```
Monthly:
```text
outdated skills, knowledge graph orphans, memory architecture audit, maturity scoring, stale rule archive
```
## Report format
```text
OpenClaw AgentOS Health Report
score: 84/100
P0: 0
P1: 2
P2: 6
completed checks:
issues:
auto-fixable:
needs confirmation:
manual:
next actions:
```
FILE:references/knowledge-base.md
# Personal Knowledge Base / Obsidian-Friendly Vault
## Principle
The vault is Markdown-first. Obsidian is optional.
- Without Obsidian: OpenClaw can still read/write Markdown.
- With Obsidian: user can visualize graph, backlinks, tags, and relationships.
## Recommended vault
```text
memory/wiki/
├── 00 Inbox/
├── 01 Projects/
├── 02 Areas/
├── 03 Resources/
├── 04 Concepts/
├── 05 People/
├── 06 Decisions/
├── 07 Workflows/
├── 08 Skills/
├── 09 Reviews/
└── 99 Archive/
```
## Karpathy-style three-layer knowledge flow
```text
Raw Capture → Distilled Knowledge → Actionable Workflow
```
Operational flow:
```text
Capture → Distill → Link → Operationalize → Archive
```
Mapping:
```text
00 Inbox raw capture
04 Concepts atomic knowledge cards
01 Projects project context
07 Workflows SOP / process / templates
08 Skills skill drafts and skill design
99 Archive stale/old material
```
## Obsidian instructions
Tell user:
```text
1. Install Obsidian: https://obsidian.md
2. Open Obsidian.
3. Choose “Open folder as vault”.
4. Select memory/wiki/.
5. Use Graph View to see knowledge graph.
```
Do not write `.obsidian/` by default. Only create optional Obsidian config if user explicitly chooses `--with-obsidian`.
## Knowledge-base health checks
Check:
```text
Inbox pile-up, empty notes, orphan notes, broken links, duplicate topics,
stale files, unlinked resources, high-frequency tasks not converted to SOP,
old SkillDrafts, unprocessed ContextCaptures.
```
FILE:references/skill-baseline.md
# Skill Baseline
## Install policy
- Install only from allowlisted names/sources.
- Show plan before installing.
- Mark API-key requirements.
- Non-ClawHub skills require explicit link + manual/confirmed install.
- After install, verify `SKILL.md` exists.
## A. Survival package: skill discovery / install / docs
```text
clawhub
find-skill
openclawmp
markdown
```
| Skill | Purpose | Source |
|---|---|---|
| `clawhub` | Search/install/update/publish skills | `npm i -g clawhub`; `clawhub install <skill>` |
| `find-skill` | Skill search + local file search | ClawHub/local |
| `openclawmp` | OpenClaw asset market guidance | local/market |
| `markdown` | Markdown docs/memory/kb maintenance | ClawHub/local |
## B. Web search / research
```text
mcp-skill
tavily
china-web-search
multi-search-engine
just-scrape
hv-analysis
```
- `mcp-skill`: Exa search/deep research/code/company research; may need `MCP_API_KEY`.
- `tavily`: web search; may need Tavily API key.
- `china-web-search`: Chinese web search.
- `multi-search-engine`: broad multi-engine search.
- `just-scrape`: page scraping.
- `hv-analysis`: systematic deep research / competitive analysis.
## C. Document processing
```text
docx
pdf
excel
pptx
markdown
summarize-1
```
- If no `docx` skill exists, use/offer a `python-docx` or mammoth-based template.
- If no `excel` skill exists, use/offer `pandas`/`openpyxl`, `data-analysis`, or sheets-related skills.
- `pdf`, `pptx`, `markdown`, `summarize-1` are strongly recommended.
## D. Summary / humanized writing
```text
summarize-1
humanizer / afrexai-humanizer-1
khazix-writer
copywriting
```
## E. System / self-evolution
```text
skill-vetter
self-improving-1
agent-autonomy-kit
knowledge-health-checker
control-mirror
openclaw-engineering-lifecycle
```
## F. Programming / engineering
```text
superpowers
code-review
gstack-openclaw-investigate
changelog-generator
mcp-builder
```
## G. Frontend / UI / product
```text
frontend
local-frontend-design
superdesign-ui
superdesign
seo-audit
```
## H. Skill creation / optimization lab
```text
skill-creator
nuwa-skill
darwin-skill
skill-vetter
self-improving-1
```
### nuwa-skill
GitHub:
```text
https://github.com/alchaincyf/nuwa-skill
```
Install:
```bash
npx skills add alchaincyf/nuwa-skill
```
### darwin-skill
GitHub:
```text
https://github.com/alchaincyf/darwin-skill
```
Install:
```bash
npx skills add alchaincyf/darwin-skill
```
Backup zip:
```text
https://pub-161ae4b5ed0644c4a43b5c6412287e03.r2.dev/skills/darwin-skill.zip
```
## Recommended packages
### bootstrap-minimal
```text
clawhub
find-skill
openclawmp
markdown
```
### bootstrap-search
```text
mcp-skill
tavily
china-web-search
multi-search-engine
just-scrape
hv-analysis
```
### bootstrap-docs
```text
docx
pdf
excel
pptx
markdown
summarize-1
```
### bootstrap-agentos-core
```text
agent-autonomy-kit
skill-creator
skill-vetter
self-improving-1
knowledge-health-checker
control-mirror
openclaw-engineering-lifecycle
```
### bootstrap-engineering
```text
superpowers
code-review
gstack-openclaw-investigate
changelog-generator
mcp-builder
```
### bootstrap-design
```text
frontend
local-frontend-design
superdesign-ui
superdesign
seo-audit
```
### bootstrap-creator
```text
khazix-writer
copywriting
humanizer
```
### bootstrap-skill-lab
```text
skill-creator
nuwa-skill
skill-vetter
darwin-skill
self-improving-1
```
FILE:scripts/diagnose.py
#!/usr/bin/env python3
"""Lightweight OpenClaw AgentOS diagnostic helper."""
from __future__ import annotations
import json, os, shutil, subprocess
from pathlib import Path
workspace = Path(os.environ.get("OPENCLAW_WORKSPACE", "/Users/mac/.openclaw/workspace")).expanduser()
skills_dir = Path(os.environ.get("OPENCLAW_SKILLS", "/Users/mac/.openclaw/skills")).expanduser()
protected = ["AGENTS.md", "MEMORY.md", "SOUL.md", "TOOLS.md", "USER.md", "DREAMS.md"]
def exists(p: Path): return p.exists()
def count_skills():
if not skills_dir.exists(): return 0
return sum(1 for p in skills_dir.iterdir() if p.is_dir())
def cmd_exists(name: str): return shutil.which(name) is not None
report = {
"workspace": str(workspace),
"workspace_exists": exists(workspace),
"skills_dir": str(skills_dir),
"skills_dir_exists": exists(skills_dir),
"skill_count": count_skills(),
"bins": {"clawhub": cmd_exists("clawhub"), "git": cmd_exists("git"), "npm": cmd_exists("npm")},
"protected_files": {name: exists(workspace / name) for name in protected},
"memory_dirs": {
"memory": exists(workspace / "memory"),
"memory/wiki": exists(workspace / "memory" / "wiki"),
"memory/projects": exists(workspace / "memory" / "projects"),
"memory/domains": exists(workspace / "memory" / "domains"),
"memory/archive": exists(workspace / "memory" / "archive"),
},
}
print(json.dumps(report, ensure_ascii=False, indent=2))
FILE:test-prompts.json
[
{
"id": "diagnose-fresh-user",
"prompt": "我刚安装 OpenClaw,什么 skill 都没有,帮我初始化成个人 AgentOS。",
"expected": "先做 Stage 0 基础自举和诊断,检查 clawhub/find-skill/search 能力,给出安全安装计划,再初始化记忆和知识库。"
},
{
"id": "memory-kb-obsidian",
"prompt": "帮我搭建三层记忆和 Obsidian 个人知识库,但不要覆盖我已有 MEMORY.md。",
"expected": "保护已有核心文件,生成 patch/备份建议,创建 HOT/WARM/COLD 与 memory/wiki vault,说明 Obsidian 可选。"
},
{
"id": "health-repair",
"prompt": "帮我一键检查并修复 OpenClaw 上下文污染和 skill 问题。",
"expected": "先诊断,输出 safe-fix/needs-confirm/manual 分类,修复前确认高风险项,最后给健康报告。"
}
]
FILE:assets/templates/MEMORY_POLICY.md
# Memory Policy
- HOT: short, permanent, high-frequency. Keep in MEMORY.md.
- WARM: project/domain/person/preference details. Keep in memory/*.
- COLD: raw logs, archives, old reports. Keep in memory/archive, logs, raw.
- Do not put temporary task details into HOT memory.
FILE:assets/templates/KNOWLEDGE_BASE_README.md
# Personal Knowledge Base
This vault is Markdown-first and Obsidian-friendly.
Flow:
```text
Capture → Distill → Link → Operationalize → Archive
```
Open this folder as an Obsidian vault if you want graph visualization.
5GC Web仪表自动化技能,支持AMF/UDM/AUSF/SMF/PGW-C/UPF/PGW-U/GNB/UE/PCF/NRF/QoS/TC/PCC/smpolicy的批量添加与编辑及PCF默认规则一键配置
---
name: 5gc-web-dotouch
version: 1.0.0
description: 5GC Web仪表自动化技能,支持AMF/UDM/AUSF/SMF/PGW-C/UPF/PGW-U/GNB/UE/PCF/NRF/QoS/TC/PCC/smpolicy的批量添加与编辑及PCF默认规则一键配置
author: liuwei120
tags: [5gc, automation, playwright, network]
---
# 5GC Web 仪表自动化技能
> 统一管理 AMF、UDM/AUSF、SMF/PGW-C、UPF/PGW-U、GNB、UE、PCF、NRF 八类网元的添加与编辑操作,以及 PCC 规则、QoS 模板、Traffic Control、SMPolicy 和 PCF 默认规则一键配置。
---
## 目录
- [快速开始](#快速开始)
- [统一 CLI 入口](#统一-cli-入口)
- [技能详细文档](#技能详细文档)
- [AMF](#amf)
- [UDM/AUSF](#udmausf)
- [SMF/PGW-C](#smfpgw-c)
- [UPF/PGW-U](#upfpgw-u)
- [GNB](#gnb)
- [UE](#ue)
- [PCF/PCRF](#pcfpcrf)
- [PCC 规则](#pcc-规则)
- [QoS 模板](#qos-模板)
- [Traffic Control](#traffic-control)
- [SMPolicy](#smpolicy)
- [UE Smpolicy](#smpolicy-ue-add-skilljs)
- [DNN Smpolicy](#smpolicy-dnn)
- [DNN Smpolicy](#smpolicy-dnn)
- [TAC Smpolicy](#smpolicy-tac)
- [Cell Smpolicy](#smpolicy-cell)
- [Cell Forbidden Smpolicy](#smpolicy-cell-forbidden)
- [NRF](#nrf)
- [全局参数参考](#全局参数参考)
- [字段参考](#字段参考)
---
## 快速开始
### 安装方法
技能目录位于 `skills/5gc/`,由统一入口 `5gc.js` 统一调度,无需额外安装:
```bash
# 克隆或复制到本机
git clone <repo> ~/.openclaw/workspace/skills/5gc
# 直接使用统一入口(推荐)
node skills/5gc/scripts/5gc.js <entity> <action> [options]
# 或直接调用各脚本
node skills/5gc/scripts/amf-add-skill.js <参数>
```
### 前置要求
- Node.js ≥ 14
- Playwright(`npm i playwright && npx playwright install chromium`)
- 5GC 仪表地址:`https://192.168.3.89`(默认)
- 登录凭证:`[email protected]` / `dotouch`
- 仪表上已创建对应工程(如 `XW_S5GC_1`)
### 会话缓存
所有脚本自动复用 Playwright 会话缓存(`.sessions/` 目录),首次登录后再次运行无需重复登录。
---
## 统一 CLI 入口
### 路径
```
node skills/5gc/scripts/5gc.js <entity> <action> [options]
```
### 支持的网元与操作
| entity | add | edit | 特殊操作 |
|--------|-----|------|---------|
| `amf` | ✅ | ✅ | |
| `udm` | ✅ | ✅ | |
| `smf` | ✅ | ✅ | |
| `upf` | ✅ | ✅ | |
| `gnb` | ✅ | ✅ | |
| `ue` | ✅ | ✅ | |
| `pcf` | ✅ | ✅ | `default-rule-add` |
| `pcc` | ✅ | ✅ | |
| `qos` | ✅ | | |
| `tc` | ✅ | | |
| `smpolicy` | | | `add-pcc`, `ue-add`, `ue-edit`, `dnn-add`, `dnn-edit` |
| `nrf` | ✅ | ✅ | |
### 全局选项
| 选项 | 说明 |
|------|------|
| `--url <地址>` | 5GC 仪表地址,默认 `https://192.168.3.89` |
| `--headed` | 打开可见浏览器窗口(调试用) |
### 三种使用模式
```bash
# 1. 添加网元
node 5gc.js amf add <名称> [参数...]
# 2. 批量编辑(当前工程下所有该类网元)
node 5gc.js amf edit --project <工程> --set-<字段> <值>
# 3. 单个编辑(按名称精确匹配)
node 5gc.js amf edit --name <名称> --project <工程> --set-<字段> <值>
```
---
## 技能详细文档
---
### AMF
#### amf-add-skill.js
**功能**:在指定工程下添加一个 AMF 实例。
**使用方式**:
```bash
node 5gc.js amf add <名称> [选项...]
# 或直接调用
node skills/5gc/scripts/amf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | AMF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `5G_basic_process` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--mcc <值>` | MCC(移动国家码) | `460` |
| `--mnc <值>` | MNC(移动网络码) | `01` |
| `--ngap_sip <IP>` | NGAP 信令面 IP | `200.20.20.1` |
| `--ngap_port <端口>` | NGAP 端口 | `38412` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `200.20.20.5` |
| `--http2_port <端口>` | HTTP2 端口 | `8080` |
| `--stac <值>` | 起始 TAC | `101` |
| `--etac <值>` | 结束 TAC | `102` |
| `--region_id <值>` | 区域 ID | `1` |
| `--set_id <值>` | Set ID | `1` |
| `--pointer <值>` | 指针 | `1` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
# 基本添加
node 5gc.js amf add AMF_TEST --project XW_S5GC_1
# 指定 NGAP IP 和端口
node 5gc.js amf add AMF_PROD --project XW_S5GC_1 --ngap_sip 10.200.1.50 --ngap_port 38412
# 使用不同 MCC/MNC
node 5gc.js amf add AMF_CMCC --project XW_S5GC_1 --mcc 460 --mnc 00
```
---
#### amf-edit-skill.js
**功能**:修改 AMF 配置参数。支持单个修改或批量修改工程下所有 AMF。
**使用方式**:
```bash
node 5gc.js amf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` / `-p <工程>` | 目标工程,不带 `--name` 时批量修改该工程下所有 AMF |
| `--name <名称>` | 精确匹配要修改的 AMF 名称 |
| `--id <ID>` | 按 AMF ID 修改 |
| `--set-<字段> <值>` | 修改指定字段的值,支持多组 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**可编辑字段**:`name`, `mcc`, `mnc`, `ngap_sip`, `ngap_port`, `http2_sip`, `http2_port`, `stac`, `etac`, `region_id`, `set_id`, `pointer`, `ea[NEA0]`, `ea[128-NEA1]`, `ea[128-NEA2]`, `ea[128-NEA3]`, `ia[NIA0]`, `ia[128-NIA1]`, `ia[128-NIA2]`, `ia[128-NIA3]`
> ⚠️ `ea[NEA0]` 等算法字段:实际向表单填入字段名 `ea[NEA0]`(input[name="ea[NEA0]"]),layui checkbox 点击基于索引而非字段名,详情见 SKILL.md 算法配置章节。
**示例**:
```bash
# 批量修改工程下所有 AMF 的 NGAP IP
node 5gc.js amf edit --project XW_S5GC_1 --set-ngap_sip 10.200.1.99
# 修改指定 AMF
node 5gc.js amf edit --name AMF_TEST --project XW_S5GC_1 --set-ngap_sip 10.200.1.50 --set-http2_sip 10.200.1.51
# 按 ID 修改
node 5gc.js amf edit --id 6633 --set-ngap_port 38413
```
---
### UDM/AUSF
#### ausf-udm-add-skill.js
**功能**:在指定工程下添加一个 UDM/AUSF 实例。
**使用方式**:
```bash
node 5gc.js udm add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | UDM 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `5G_basic_process` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--count <数量>` | 实例数量 | `1` |
| `--sip <IP>` | SIP 服务 IP | `192.168.20.30` |
| `--port <端口>` | SIP 端口 | `80` |
| `--auth_method <方法>` | 认证方法 | `5G_AKA` |
| `--scheme <协议>` | 协议类型 | `HTTP` |
| `--priority <优先级>` | 优先级 | `8` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
# 基本添加
node 5gc.js udm add UDM_TEST --project XW_S5GC_1
# 指定 SIP IP 和端口
node 5gc.js udm add UDM_PROD --project XW_S5GC_1 --sip 10.0.0.100 --port 8080
# 批量添加 3 个实例
node 5gc.js udm add UDM_CLUSTER --project XW_S5GC_1 --count 3 --sip 10.0.0.50
```
---
#### ausf-udm-edit-skill.js
**功能**:修改 UDM/AUSF 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js udm edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 UDM 名称 |
| `--set-sip <IP>` | 修改 SIP IP |
| `--set-port <端口>` | 修改端口 |
| `--set-auth_method <方法>` | 修改认证方法 |
| `--set-scheme <协议>` | 修改协议 |
| `--set-count <数量>` | 修改实例数量 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**示例**:
```bash
# 批量修改工程下所有 UDM 的 SIP IP
node 5gc.js udm edit --project XW_S5GC_1 --set-sip 10.0.0.99
# 修改指定 UDM
node 5gc.js udm edit --name UDM_TEST --project XW_S5GC_1 --set-sip 10.0.0.88 --set-port 8080
```
---
### SMF/PGW-C
#### smf-pgwc-add-skill.js
**功能**:在指定工程下添加一个 SMF/PGW-C 实例。
**使用方式**:
```bash
node 5gc.js smf add --name <名称> [选项...]
```
> ⚠️ 通过 5gc.js 统一调度时必须使用 `--name <名称>` 形式(不是位置参数)。
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--name <名称>` | SMF 实例名称 | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--pfcp_sip <IP>` | PFCP 信令面 IP | `200.20.20.25` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `200.20.20.25` |
| `--mcc <值>` | MCC | `460` |
| `--mnc <值>` | MNC | `01` |
| `--pdu_capacity <数量>` | PDU 会话容量 | `200000` |
| `--ue_min <IP>` | UE IP 池起始 | `30.30.30.20` |
| `--ue_max <IP>` | UE IP 池结束 | `30.31.30.20` |
| `--interest_tac <TAC列表>` | 关注 TAC 列表(逗号分隔) | `101,102` |
| `--headed` | 打开可见浏览器 | false |
> ✅ **NSSAI 自动配置**:脚本在 SMF 创建后会自动打开 NSSAI 配置弹窗,添加一条默认 NSSAI(SST=1, SD=000001, DNN Group=cscn2net)。如需自定义 NSSAI 参数,请直接修改脚本中的硬编码值。
>
> ⚠️ ue_sip6 / ue_eip6 为硬编码值,不支持 CLI 参数覆盖。
**示例**:
```bash
# 基本添加
node 5gc.js smf add --name SMF_TEST --project XW_S5GC_1
# 指定工程和 IP/MCC
node 5gc.js smf add --name SMF_PROD --project XW_S5GC_1 --pfcp_sip 10.10.10.50 --http2_sip 10.10.10.51 --mcc 460 --mnc 01
```
---
#### smf-pgwc-edit-skill.js
**功能**:修改 SMF/PGW-C 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js smf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 SMF 名称 |
| `--set-pfcp_sip <IP>` | 修改 PFCP 信令面 IP |
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-pdu_capacity <数量>` | 修改 PDU 会话容量 |
| `--set-ue_min <IP>` | 修改 UE IP 池起始 |
| `--set-ue_max <IP>` | 修改 UE IP 池结束 |
| `--set-interest_tac <TAC列表>` | 修改关注 TAC 列表(逗号分隔) |
> ⚠️ 以下字段不支持 `--set-` 修改:dnn、n3_ip、n6_ip、snssai_sst、snssai_sd。如需修改,请通过仪表 UI 手动完成。NSSAI 配置请在添加时自动完成(见上文)。
**示例**:
```bash
# 批量修改工程下所有 SMF 的 HTTP2 IP
node 5gc.js smf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 SMF 的 pfcp_sip 和 MCC/MNC
node 5gc.js smf edit --name SMF_TEST --project XW_S5GC_1 --set-pfcp_sip 10.10.10.88 --set-mcc 460 --set-mnc 01
```
---
### UPF/PGW-U
#### upf-add-skill.js
**功能**:在指定工程下添加一个 UPF/PGW-U 实例。
**使用方式**:
```bash
node 5gc.js upf add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | UPF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--n4_ip <IP>` | N4 接口 IP | `192.168.20.30` |
| `--n3_ip <IP>` | N3 接口 IP | `192.168.20.30` |
| `--n6_ip <IP>` | N6 接口 IP | `192.168.20.31` |
| `--n4_port <端口>` | N4 端口 | `8805` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--pdu_capacity <数量>` | PDU 会话容量 | `20000` |
| `--ue_min <IP>` | UE IP 池起始 | `20.20.20.20` |
| `--ue_max <IP>` | UE IP 池结束 | `20.20.60.20` |
| `--headed` | 打开可见浏览器 | false |
> ⚠️ DNN、TAC、NSSAI 在添加脚本中为硬编码默认值,不支持命令行覆盖。如需修改,请使用 `upf edit` 脚本。
**示例**:
```bash
# 基本添加
node 5gc.js upf add UPF_TEST --project XW_S5GC_1
# 指定 N4/N3/N6 IP 和 MCC/MNC
node 5gc.js upf add UPF_PROD --project XW_S5GC_1 --n4_ip 10.0.0.50 --n6_ip 10.0.0.51 --MCC 460 --MNC 01
```
---
#### upf-edit-skill.js
**功能**:修改 UPF/PGW-U 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js upf edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 UPF 名称 |
| `--set-n3_ip <IP>` | 修改 N3 接口 IP |
| `--set-n4_ip <IP>` | 修改 N4 接口 IP |
| `--set-n4_port <端口>` | 修改 N4 端口 |
| `--set-n6_ip <IP>` | 修改 N6 接口 IP |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
| `--set-pdu_capacity <数量>` | 修改 PDU 会话容量 |
| `--set-ue_min <IP>` | 修改 UE IP 池起始 |
| `--set-ue_max <IP>` | 修改 UE IP 池结束 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
> ⚠️ `dnn`(DNN)和 TAC/NSSAI 在 UPF 表单中存储在 jsgrid 配置行内,不支持简单的 `--set-` 修改。
**示例**:
```bash
# 批量修改工程下所有 UPF 的 N4 IP
node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 99.99.99.99
# 修改指定 UPF 的 N4/N6 IP 和 MCC/MNC
node 5gc.js upf edit --name UPF_TEST --project XW_S5GC_1 --set-n4_ip 88.88.88.88 --set-n6_ip 88.88.88.89 --set-MCC 460 --set-MNC 01
```
---
### GNB
#### gnb-add-skill.js
**功能**:在指定工程下添加一个 GNB 实例。
**使用方式**:
```bash
node 5gc.js gnb add <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | GNB 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--ngap_sip <IP>` | NGAP 信令面 IP | `200.20.20.50` |
| `--user_sip_ip_v4 <IP>` | 用户面 IPv4 | `2.2.2.2` |
| `--mcc <值>` | MCC | `460` |
| `--mnc <值>` | MNC | `60` |
| `--stac <值>` | 起始 TAC | `0` |
| `--etac <值>` | 结束 TAC | `0` |
| `--node_id <ID>` | 节点 ID | `70` |
| `--cell_count <数量>` | 小区数量 | `1` |
| `--headed` | 打开可见浏览器 | false |
> ⚠️ `stac`/`etac`/`node_id` 非默认值时可能触发表单验证失败,建议先使用默认值添加后再用 `gnb edit` 修改。
**示例**:
```bash
# 基本添加
node 5gc.js gnb add GNB_TEST --project XW_S5GC_1
# 指定 NGAP IP、用户面 IP 和 TAC
node 5gc.js gnb add GNB_PROD --project XW_S5GC_1 --ngap_sip 200.20.20.100 --user_sip_ip_v4 3.3.3.3 --mcc 460 --mnc 60 --stac 1 --etac 10
```
---
#### gnb-edit-skill.js
**功能**:修改 GNB 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js gnb edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改 |
| `--name <名称>` | 精确匹配要修改的 GNB 名称 |
| `--set-ngap_sip <IP>` | 修改 NGAP 信令面 IP |
| `--set-user_sip_ip_v4 <IP>` | 修改用户面 IPv4 |
| `--set-user_sip_ip_v6 <IP>` | 修改用户面 IPv6 |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-stac <值>` | 修改起始 TAC |
| `--set-etac <值>` | 修改结束 TAC |
| `--set-node_id <ID>` | 修改节点 ID |
| `--set-cell_count <数量>` | 修改小区数量 |
| `--set-replay_ip <IP>` | 修改回放 IP |
| `--set-replay_port <端口>` | 修改回放端口 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
**示例**:
```bash
# 批量修改工程下所有 GNB 的用户面 IP
node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 99.99.99.99
# 修改指定 GNB 的 NGAP IP 和 MCC/MNC
node 5gc.js gnb edit --name GNB_TEST --project XW_S5GC_1 --set-ngap_sip 200.20.20.88 --set-mcc 461 --set-mnc 22
```
---
### UE
#### ue-add-skill.js
**功能**:在指定工程下添加一个或多个 UE 实例。
**使用方式**:
```bash
node 5gc.js ue add --name <名称> [选项...]
```
**参数**:
| 参数 | 短名 | 说明 | 默认值 |
|------|------|------|--------|
| `--name <名称>` | `-n <名称>` | UE 名称(只支持字母/数字/下划线) | **必填** |
| `--project <工程>` | `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | `-u <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--imsi <值>` | | 起始 IMSI(15位) | `460001234567890` |
| `--msisdn <值>` | | MSISDN(13-15位,以 86 开头) | `8611111111111` |
| `--mcc <值>` | | MCC | `460` |
| `--mnc <值>` | | MNC | `01` |
| `--key <值>` | | KI 密钥(32位 hex) | `1111...`(32个1) |
| `--opc <值>` | | OPc 密钥(32位 hex) | `1111...`(32个1) |
| `--imeisv <值>` | | IMEISV(偶数位) | `8611111111111111` |
| `--sst <值>` | | NSSAI SST | `1` |
| `--sd <值>` | | NSSAI SD | `111111` |
| `--count <数量>` | `-c <数量>` | 连续添加数量 | `1` |
| `--headed` | | 打开可见浏览器 | false |
> **命名约束**:UE 名称只能包含字母、数字、下划线(`_`),不能使用连字符(`-`)或其他特殊字符。
**示例**:
```bash
# 基本添加
node 5gc.js ue add --name UE_001 --project XW_S5GC_1
# 指定 IMSI 和 MSISDN
node 5gc.js ue add --name UE_TEST --imsi 460000000000001 --msisdn 8613888888888 --project XW_S5GC_1
# 批量添加 10 个连续 UE
node 5gc.js ue add --name UE_BATCH --count 10 --project XW_S5GC_1 --msisdn 8613900000000
# 指定认证密钥
node 5gc.js ue add --name UE_AUTH --project XW_S5GC_1 --key 00112233445566778899aabbccddeeff --opc 11223344556677889900aabbccddeeff
```
---
#### ue-edit-skill.js
**功能**:修改 UE 配置参数。支持批量和单个修改。
**使用方式**:
```bash
node 5gc.js ue edit [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project <工程>` | 目标工程,不带 `--name` 时批量修改该工程下所有 UE |
| `--name <名称>` | 精确匹配要修改的 UE 名称(不支持批量时按名称过滤) |
| `--id <ID>` | 按 UE ID 修改 |
| `--set-msisdn <值>` | 修改 MSISDN |
| `--set-s_imsi <值>` | 修改 IMSI |
| `--set-mcc <值>` | 修改 MCC |
| `--set-mnc <值>` | 修改 MNC |
| `--set-key <值>` | 修改 KI 密钥 |
| `--set-opc <值>` | 修改 OPc 密钥 |
| `--set-imeisv <值>` | 修改 IMEISV |
| `--set-sst <值>` | 修改 NSSAI SST |
| `--set-sd <值>` | 修改 NSSAI SD |
| `--set-replay_ip <IP>` | 修改回放 IP |
| `--set-replay_port <端口>` | 修改回放端口 |
| `--set-count <数量>` | 修改数量 |
| `--url <地址>` | 5GC 仪表地址 |
| `--headed` | 打开可见浏览器 |
> ⚠️ `user_sip_ip_v4`、`user_sip_ip_v6` 在 UE 编辑表单中不存在此字段名,无需修改。
**示例**:
```bash
# 批量修改工程下所有 UE 的 MSISDN
node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613911111111
# 修改指定 UE
node 5gc.js ue edit --name UE_001 --project XW_S5GC_1 --set-msisdn 8613988888888 --set-sst 1 --set-sd 222222
# 按 ID 修改
node 5gc.js ue edit --id 10337 --set-opc aabbccddeeff00112233445566778899 --set-imeisv 8611111111111112
```
---
### PCF/PCRF
#### pcf-add-skill.js
**功能**:在指定工程下添加一个 PCF/PCRF 实例。
**使用方式**:
```bash
node 5gc.js pcf add <名称> [选项...]
node skills/5gc/scripts/pcf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | PCF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `192.168.20.90` |
| `--http2_port <端口>` | HTTP2 端口 | `80` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
node 5gc.js pcf add PCF-TEST --project XW_S5GC_1
node 5gc.js pcf add PCF-PROD --project XW_S5GC_1 --http2_sip 10.0.0.50 --MCC 460 --MNC 01
```
#### pcf-edit-skill.js
**功能**:编辑指定工程下的 PCF/PCRF 实例(支持单条和批量)。
**使用方式**:
```bash
# 批量编辑:修改工程下所有 PCF 的字段
node 5gc.js pcf edit --project <工程> --set-<字段> <值>
# 单条编辑:修改指定名称的 PCF
node 5gc.js pcf edit --name <名称> --project <工程> --set-<字段> <值>
```
**可编辑字段**:
| 参数 | 说明 |
|------|------|
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-http2_port <端口>` | 修改 HTTP2 端口 |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
**示例**:
```bash
# 批量修改工程下所有 PCF 的 HTTP2 IP
node 5gc.js pcf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 PCF 的 HTTP2 IP 和 MNC
node 5gc.js pcf edit --name pcc --project XW_S5GC_1 --set-http2_sip 10.10.10.88 --set-MNC 01
```
#### default-rule-add-skill.js(PCF 默认规则一键配置)
**功能**:为指定工程一键配置完整的 PCF 默认规则链路,包含 QoS 模板 → Traffic Control → PCC 规则 → sm_policy_default → PCF default_smpolicy 全五步。
**使用方式**:
```bash
node 5gc.js pcf default-rule-add --project <工程> [选项...]
node skills/5gc/scripts/default-rule-add-skill.js --project <工程> [选项...]
```
**参数**(全部可选,有默认值):
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--qos-id` | QoS 模板 ID | `qos_default_{时间戳}` |
| `--5qi` | 5QI 值(不指定则自动选择未使用的值) | 自动(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率 | `10000000` |
| `--maxbr-dl` | 下行最大比特率 | `20000000` |
| `--gbr-ul` | 上行保证比特率 | `5000000` |
| `--gbr-dl` | 下行保证比特率 | `5000000` |
| `--tc-id` | TC 规则 ID | `tc_default_{时间戳}` |
| `--flow-status` | TC 流状态 | `ENABLED` |
| `--pcc-id` | PCC 规则 ID | `pcc_default` |
| `--precedence` | PCC 优先级 | `63` |
| `--headed` | 显示浏览器窗口(调试用) | off |
**示例**:
```bash
# 最简用法(自动生成所有 ID)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4
# 指定 QoS 参数(高速率)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 \
--qos-id qos_high_rate --5qi 8 \
--maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 指定 PCC 优先级
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --precedence 50
# 调试模式
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --headed
```
> **注意**:同一工程多次运行会自动删除旧的同名资源并重建,不会污染配置。
### PCC 规则
#### pcc-add-skill.js
**功能**:在指定工程下添加一条 PCC 规则(PCC 规则用于绑定 QoS 模板和 Traffic Control)。
**使用方式**:
```bash
node 5gc.js pcc add --project <工程> --pcc-id <ID> --qos <QoS名称> --tc <TC名称> [选项...]
node skills/5gc/scripts/pcc-add-skill.js --project <工程> --pcc-id <ID> --qos <QoS名称> --tc <TC名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--pcc-id` | PCC 规则 ID(字母/数字/下划线) | **必填** |
| `--qos` | 引用的 QoS 模板名称 | **必填** |
| `--tc` | 引用的 Traffic Control 名称 | **必填** |
| `--precedence` | 优先级(0-255) | `63` |
| `--flow-desc` | 流描述(可选) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
# 指定优先级
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_high --qos qos2 --tc tc1 --precedence 50
```
#### pcc-edit-skill.js
**功能**:编辑已有 PCC 规则的 QoS/TC 绑定(切换 PCC 引用的 QoS 模板或 Traffic Control)。
**使用方式**:
```bash
node 5gc.js pcc edit --project <工程> --name <PCC名称> --set-qos <新QoS> [--set-tc <新TC>]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要修改的 PCC 规则名称(精确匹配) |
| `--set-qos <名称>` | 切换到新的 QoS 模板 |
| `--set-tc <名称>` | 切换到新的 Traffic Control |
| `--headed` | 显示浏览器窗口 |
**示例**:
```bash
# 修改 PCC 引用的 QoS(用于修改上下行速率)
node 5gc.js pcc edit --project XW_SUPF_5_1_2_4 --name pcc_default --set-qos qos_high_rate
# 同时修改 QoS 和 TC
node 5gc.js pcc edit --project XW_SUPF_5_1_2_4 --name pcc_default --set-qos qos1 --set-tc tc2
```
> ⚠️ **重要**:PCC 的 `refQosData` 和 `refTcData` 存储在 xm-select 多选组件中。编辑时会自动切换选择,无需手动取消旧选项。
### NRF(网络存储功能)
#### nrf-add-skill.js
**功能**:在指定工程下添加一个 NRF 实例。
**使用方式**:
```bash
node 5gc.js nrf add <名称> [选项...]
node skills/5gc/scripts/nrf-add-skill.js <名称> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<名称>` | NRF 实例名称(位置参数) | **必填** |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | `XW_S5GC_1` |
| `--url <地址>` | 5GC 仪表地址 | `https://192.168.3.89` |
| `--http2_sip <IP>` | HTTP2 服务 IP | `192.168.20.100` |
| `--http2_port <端口>` | HTTP2 端口 | `80` |
| `--MCC <值>` | MCC(注意大写) | `460` |
| `--MNC <值>` | MNC(注意大写) | `01` |
| `--headed` | 打开可见浏览器 | false |
**示例**:
```bash
node 5gc.js nrf add NRF-TEST --project XW_S5GC_1
node 5gc.js nrf add NRF-PROD --project XW_S5GC_1 --http2_sip 10.0.0.50 --MCC 460 --MNC 01
```
#### nrf-edit-skill.js
**功能**:编辑指定工程下的 NRF 实例(支持单条和批量)。
**使用方式**:
```bash
# 批量编辑:修改工程下所有 NRF 的字段
node 5gc.js nrf edit --project <工程> --set-<字段> <值>
# 单条编辑:修改指定名称的 NRF
node 5gc.js nrf edit --name <名称> --project <工程> --set-<字段> <值>
```
**可编辑字段**:
| 参数 | 说明 |
|------|------|
| `--set-http2_sip <IP>` | 修改 HTTP2 服务 IP |
| `--set-http2_port <端口>` | 修改 HTTP2 端口 |
| `--set-MCC <值>` | 修改 MCC(注意大写) |
| `--set-MNC <值>` | 修改 MNC(注意大写) |
**示例**:
```bash
# 批量修改工程下所有 NRF 的 HTTP2 IP
node 5gc.js nrf edit --project XW_S5GC_1 --set-http2_sip 10.10.10.99
# 修改指定 NRF 的 HTTP2 IP 和 MNC
node 5gc.js nrf edit --name nrf1 --project XW_S5GC_1 --set-http2_sip 10.10.10.88 --set-MNC 01
```
### QoS 模板
#### qos-add-skill.js
**功能**:在指定工程下添加一个 QoS(服务质量)模板,定义 5QI、上下行最大比特率、保证比特率等参数。
**使用方式**:
```bash
node 5gc.js qos add --project <工程> --qos-id <ID> [选项...]
node skills/5gc/scripts/qos-add-skill.js --project <工程> --qos-id <ID> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--qos-id` | QoS 模板 ID(字母/数字/下划线) | **必填** |
| `--5qi` | 5QI 值(不指定则自动选择) | 自动选择未使用的值(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率(bps) | `10000000` |
| `--maxbr-dl` | 下行最大比特率(bps) | `20000000` |
| `--gbr-ul` | 上行保证比特率(bps) | `5000000` |
| `--gbr-dl` | 下行保证比特率(bps) | `5000000` |
| `--priority` | 优先级 | 空 |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos1
# 高速率 QoS
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_high \
--5qi 8 --maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 批量创建不同 5qi 的 QoS 模板
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_6 --5qi 6
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_9 --5qi 9
```
---
### Traffic Control
#### tc-add-skill.js
**功能**:在指定工程下添加一条 Traffic Control 流量控制规则,控制 UE 流量的启用/禁用状态。
**使用方式**:
```bash
node 5gc.js tc add --project <工程> --tc-id <ID> [选项...]
node skills/5gc/scripts/tc-add-skill.js --project <工程> --tc-id <ID> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--tc-id` | TC 规则 ID(字母/数字/下划线) | **必填** |
| `--flow-status` | 流状态 | `ENABLED` |
| `--flow-desc` | 流描述(可选) | |
| `--headed` | 显示浏览器窗口 | off |
> **flow-status 选项**:`ENABLED`(启用)、`DISABLED`(禁用)、`ENABLED-UPLINK`(仅上行)等。
**示例**:
```bash
# 基本添加
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc1
# 指定流状态
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_uplink --flow-status ENABLED-UPLINK
```
---
### SMPolicy
#### smpolicy_add_pcc.js {#smpolicy-default}
**功能**:将已有 PCC 规则添加到工程默认的 `sm_policy_default` 会话策略中(追加到 pccRules 列表)。
**使用方式**:
```bash
node 5gc.js smpolicy add-pcc --project <工程> --pcc-id <PCC名称>
node skills/5gc/scripts/smpolicy_add_pcc.js --project <工程> --pcc-id <PCC名称>
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_SUPF_5_1_2_4` |
| `--pcc-id` | 已存在的 PCC 规则 ID | **必填** |
| `--headed` | 显示浏览器窗口 | off |
> **链路**:`smpolicy/default/index` → 编辑 `sm_policy_default` 弹窗 → pccRules xm-select 中追加指定 PCC。
**示例**:
```bash
# 将 PCC 添加到 sm_policy_default
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_high_rate
# 添加多个 PCC 规则
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_default
node 5gc.js smpolicy add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_video
```
---
#### smpolicy-ue-add-skill.js {#smpolicy-ue-add-skilljs}
**功能**:在指定工程下添加一条 UE Smpolicy 规则,按 IMSI/DNN/sNssai 匹配 UE 并关联 PCC 规则。
**使用方式**:
```bash
node 5gc.js smpolicy ue-add --project <工程> --name <名称> --dnn <DNN> [选项...]
node skills/5gc/scripts/smpolicy-ue-add-skill.js --project <工程> --name <名称> --dnn <DNN> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--name` | UE策略名称(字母/数字/下划线) | **必填** |
| `--dnn` | DNN | **必填** |
| `--imsi` | IMSI 起始值(不填则自动生成) | 自动 |
| `--imsi-num` | IMSI 数量 | `1` |
| `--sst` | sNssai SST | `1` |
| `--sd` | sNssai SD | `111111` |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) | |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) | |
| `--pra-rules` | PRA规则(xm-select,可选) | |
| `--ref-qos-timer` | reflectiveQoSTimer 值(秒) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet
# 指定 IMSI 和 sNssai
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet \
--imsi 460001234567890 --sst 1 --sd 111111
# 绑定 PCC 规则(多个逗号分隔)
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy2 --dnn internet \
--pcc-rules "pcc2,pcc_default"
# 指定反射 QoS 定时器
node 5gc.js smpolicy ue-add --project XW_SUPF_5_1_2_4 --name ue_policy3 --dnn internet \
--pcc-rules pcc2 --ref-qos-timer 60
```
#### smpolicy-ue-edit-skill.js
**功能**:编辑已有 UE Smpolicy 规则的字段(DNN、sNssai、PCC 绑定等)。
**使用方式**:
```bash
node 5gc.js smpolicy ue-edit --project <工程> --name <名称> [--dnn <新DNN>] [--pcc-rules <规则>] [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要编辑的 UE 策略名称(精确匹配) |
| `--dnn` | 新 DNN |
| `--imsi` | 新 IMSI 起始值 |
| `--sst` | 新 sNssai SST |
| `--sd` | 新 sNssai SD |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) |
| `--pra-rules` | PRA规则(xm-select) |
| `--ref-qos-timer` | reflectiveQoSTimer |
| `--headed` | 显示浏览器窗口 |
> ⚠️ xm-select 为多选模式。指定 `--pcc-rules` 时会叠加选中已有规则;编辑时需注意 toggle 行为。
**示例**:
```bash
# 修改 DNN
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --dnn internet_new
# 修改 PCC 绑定
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --pcc-rules pcc2,pcc_reg_test
# 修改 sNssai
node 5gc.js smpolicy ue-edit --project XW_SUPF_5_1_2_4 --name ue_policy1 --sst 1 --sd 222222
```
#### smpolicy-dnn-add-skill.js {#smpolicy-dnn}
**功能**:在指定工程下添加一条 DNN Smpolicy 规则,按 DNN/sNssai 匹配会话并关联 PCC 规则。
**使用方式**:
```bash
node 5gc.js smpolicy dnn-add --project <工程> --name <名称> --dnn <DNN> [选项...]
```
**参数**:
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--name` | DNN策略名称(必填) | **必填** |
| `--dnn` | DNN值(必填) | **必填** |
| `--sst` | sNssai SST | `1` |
| `--sd` | sNssai SD | `111111` |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) | |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) | |
| `--pra-rules` | PRA规则(xm-select,可选) | |
| `--ref-qos-timer` | reflectiveQoSTimer(秒) | |
| `--headed` | 显示浏览器窗口 | off |
**示例**:
```bash
# 基本添加
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet
# 绑定 PCC 规则
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet --pcc-rules pcc2
# 多个 PCC 规则
node 5gc.js smpolicy dnn-add --project XW_SUPF_5_1_2_4 --name dnn_policy2 --dnn internet --pcc-rules "pcc2,pcc_default"
```
#### smpolicy-dnn-edit-skill.js
**功能**:编辑已有 DNN Smpolicy 规则的字段(DNN、sNssai、PCC 绑定等)。
**使用方式**:
```bash
node 5gc.js smpolicy dnn-edit --project <工程> --name <名称> [--dnn <新DNN>] [--pcc-rules <规则>] [选项...]
```
**参数**:
| 参数 | 说明 |
|------|------|
| `--project` | 工程名 |
| `--name` | 要编辑的 DNN 策略名称(精确匹配) |
| `--dnn` | 新 DNN 值 |
| `--sst` | 新 sNssai SST |
| `--sd` | 新 sNssai SD |
| `--sess-rules` | 会话规则(xm-select,多个逗号分隔) |
| `--pcc-rules` | PCC规则(xm-select,多个逗号分隔) |
| `--pra-rules` | PRA规则(xm-select) |
| `--ref-qos-timer` | reflectiveQoSTimer |
| `--headed` | 显示浏览器窗口 |
> ⚠️ xm-select 为多选模式。指定 `--pcc-rules` 时会叠加选中已有规则;编辑时需注意 toggle 行为。
**示例**:
```bash
# 修改 DNN
node 5gc.js smpolicy dnn-edit --project XW_SUPF_5_1_2_4 --name dnn_policy1 --dnn internet_new
# 修改 PCC 绑定
node 5gc.js smpolicy dnn-edit --project XW_SUPF_5_1_2_4 --name dnn_policy1 --pcc-rules pcc2,pcc_default
```
---
## 全局参数参考
以下参数所有脚本均支持:
| 参数 | 说明 | 适用范围 |
|------|------|---------|
| `--url <地址>` | 5GC 仪表 URL | 所有脚本 |
| `--project <工程>` / `-p <工程>` | 目标工程名称 | 所有脚本 |
| `--headed` | 打开可见 Chromium 窗口(调试用) | 所有脚本 |
| `--set-<字段> <值>` | 修改指定字段值 | 所有 edit 脚本 |
| `--name <名称>` | 按名称精确匹配 | 所有 edit 脚本 |
| `--id <ID>` | 按 ID 直接定位 | 所有 edit 脚本 |
---
## 字段参考
### AMF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `mcc` | 移动国家码 | `460` |
| `mnc` | 移动网络码 | `01` |
| `ngap_sip` | NGAP 信令面 IP | `10.200.1.50` |
| `ngap_port` | NGAP 端口 | `38412` |
| `http2_sip` | HTTP2 服务 IP | `10.200.1.51` |
| `http2_port` | HTTP2 端口 | `8080` |
| `stac` | 起始 TAC | `101` |
| `etac` | 结束 TAC | `102` |
| `region_id` | 区域 ID | `1` |
| `set_id` | Set ID | `1` |
| `pointer` | 指针 | `1` |
| `ea[NEA0]` ~ `ea[128-NEA3]` | 加密算法(默认全选) | `1` |
| `ia[NIA0]` ~ `ia[128-NIA3]` | 完整性保护算法(默认全选) | `1` |
### UDM/AUSF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `count` | 实例数量 | `3` |
| `sip` | SIP 服务 IP | `10.0.0.100` |
| `port` | 端口 | `80` |
| `auth_method` | 认证方法 | `5G_AKA` |
| `scheme` | 协议类型 | `HTTP` |
| `priority` | 优先级 | `8` |
### SMF/PGW-C 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `pfcp_sip` | PFCP 信令面 IP | `10.10.10.50` |
| `n3_ip` | N3 接口 IP | `10.10.10.50` |
| `n6_ip` | N6 接口 IP | `10.10.10.51` |
| `http2_sip` | HTTP2 服务 IP | `10.10.10.50` |
| `dnn` | DNN(数据网络名) | `internet` |
| `snssai_sst` | NSSAI SST | `1` |
| `snssai_sd` | NSSAI SD | `ffffff` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `01` |
| `pdu_capacity` | PDU 会话容量 | `200000` |
### UPF/PGW-U 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `n3_ip` | N3 接口 IP | `192.168.20.30` |
| `n4_ip` | N4 接口 IP(PFCP) | `192.168.20.30` |
| `n6_ip` | N6 接口 IP | `192.168.20.31` |
| `n6_gw` | N6 网关 IP | `192.168.20.1` |
| `dnn` | DNN | `internet` |
| `static_arp` | 静态 ARP | `192.168.20.254` |
| `sst` | NSSAI SST | `1` |
| `sd` | NSSAI SD | `ffffff` |
| `stac` | 起始 TAC | `101` |
| `etac` | 结束 TAC | `102` |
### GNB 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `ngap_sip` | NGAP 信令面 IP | `200.20.20.50` |
| `user_sip_ip_v4` | 用户面 IPv4 | `2.2.2.2` |
| `user_sip_ip_v6` | 用户面 IPv6 | `::1` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `60` |
| `stac` | 起始 TAC | `0` |
| `etac` | 结束 TAC | `0` |
| `node_id` | 节点 ID | `70` |
| `cell_count` | 小区数量 | `1` |
| `replay_ip` | 回放 IP | `0.0.0.0` |
| `replay_port` | 回放端口 | `0` |
### UE 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `s_imsi` | 起始 IMSI(15位) | `460001234567890` |
| `msisdn` | MSISDN(13-15位,86开头) | `8613888888888` |
| `mcc` | MCC | `460` |
| `mnc` | MNC | `01` |
| `key` | KI 密钥(32位 hex) | `001122...` |
| `op_opc` | OPc 密钥(32位 hex) | `aabbcc...` |
| `imeisv` | IMEISV(15位,偶数) | `8611111111111111` |
| `nssai_sst` | NSSAI SST | `1` |
| `nssai_sd` | NSSAI SD | `111111` |
| `user_sip_ip_v4` | 用户面 IPv4 | `自动分配` |
| `user_sip_ip_v6` | 用户面 IPv6 | `自动分配` |
| `replay_ip` | 回放 IP | `0.0.0.0` |
| `replay_port` | 回放端口 | `0` |
#### default-rule-add-skill.js(PCF 默认规则一键配置)
**功能**:为指定工程一键配置完整的 PCF 默认规则链路,包含 QoS 模板 → Traffic Control → PCC 规则 → sm_policy_default → PCF default_smpolicy 全五步。
**使用方式**:
```bash
node 5gc.js pcf default-rule-add --project <工程> [选项...]
node skills/5gc/scripts/default-rule-add-skill.js --project <工程> [选项...]
```
**参数**(全部可选,有默认值):
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--project` | 工程名 | `XW_S5GC_1` |
| `--pcf-name` | **PCF 实例名称**(必填,指定要为哪个 PCF 配置默认规则) | 无 |
| `--qos-id` | QoS 模板 ID | `qos_default_{时间戳}` |
| `--5qi` | 5QI 值(不指定则自动选择未使用的值) | 自动(优先 8/9/6/5...) |
| `--maxbr-ul` | 上行最大比特率 | `10000000` |
| `--maxbr-dl` | 下行最大比特率 | `20000000` |
| `--gbr-ul` | 上行保证比特率 | `5000000` |
| `--gbr-dl` | 下行保证比特率 | `5000000` |
| `--tc-id` | TC 规则 ID | `tc_default_{时间戳}` |
| `--flow-status` | TC 流状态 | `ENABLED` |
| `--pcc-id` | PCC 规则 ID | `pcc_default` |
| `--precedence` | PCC 优先级 | `63` |
| `--headed` | 显示浏览器窗口(调试用) | off |
**示例**:
```bash
# 最简用法(自动生成所有 ID)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
# 指定 QoS 参数(高速率)
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc \
--qos-id qos_high_rate --5qi 8 \
--maxbr-ul 50000000 --maxbr-dl 100000000 \
--gbr-ul 20000000 --gbr-dl 40000000
# 指定 PCC 优先级
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --pcc-id pcc_new --precedence 50
# 调试模式
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --headed
```
**完整链路**:
1. ✅ **QoS 模板创建**:自动选择未使用的 5QI,创建 QoS 模板
2. ✅ **Traffic Control 创建**:创建 ENABLED 状态的 TC 规则
3. ✅ **PCC 规则创建**:创建 PCC 规则,绑定 QoS 和 TC
4. ✅ **sm_policy_default 创建/更新**:创建或更新默认会话策略,绑定 PCC 规则
5. ✅ **PCF default_smpolicy 设置**:为指定 PCF 实例设置 default_smpolicy 为 sm_policy_default
**注意事项**:
- 同一工程多次运行会自动删除旧的同名资源并重建,不会污染配置
- 必须指定 `--pcf-name` 参数,明确要为哪个 PCF 实例配置默认规则
- 脚本会自动处理弹窗(iframe)和 CSRF token,无需手动操作
- 所有步骤都有验证检查,确保配置成功
**已测试工程**:
- ✅ XW_SUPF_5_1_11_2(PCF "qqq")
- ✅ XW_SUPF_5_1_8_1(PCF "pcc")
- ✅ XW_SUPF_5_1_4_1(PCF "pcc")
### PCF/PCRF 字段
| 字段名 | 说明 | 示例值 |
|--------|------|--------|
| `http2_sip` | HTTP2 服务 IP | `192.168.20.90` |
| `http2_port` | HTTP2 端口 | `80` |
| `MCC` | MCC(大写) | `460` |
| `MNC` | MNC(大写) | `01` |
| `count` | 实例数量 | `1` |
FILE:scripts/5gc.js
/**
* 5GC Web 仪表统一 CLI
*
* 用法: node 5gc.js <entity> <action> [options]
*
* entity (网元类型): amf | udm | smf | upf | gnb | ue | pcf | nrf | qos | tc | smpolicy
* action (操作类型): add | edit | default-rule-add | default-rule-edit
*
* 通用选项:
* --url <地址> 5GC 仪表地址(默认 https://192.168.3.89)
* --project <工程> 目标工程名称
* --name <名称> 网元名称(用于单条记录筛选)
* --id <id> 网元 ID(直接编辑指定 ID)
* --headed 以有头模式运行(显示浏览器窗口)
*
* 字段修改(edit 模式)--set-<field> <value>:
* AMF: name|sbi_ip|sbi_port|amf_name|guami|mcc|mnc|sst|sd|ap1|ap2|ap3|ap4|ap5
* UDM: name|auth_supi|auth_op_type|op_opc|aud_method|scheme|id|priority
* SMF: name|pfcp_ip|n3_ip|n6_ip|dnn|snssai|sliceamba_type
* UPF: name|n4_ip|n3_ip|n6_ip|dnn|snssai|count|static_arp|ue_ip_pool
* GNB: name|ngap_ip|user_sip_ip_v4|mcc|mnc|stac|etac|node_id|cell_count|replay_ip|replay_port
* UE: name|count|mcc|mnc|s_imsi|key|opc|imeisv|msisdn|user_sip_ip_v4|user_sip_ip_v6|replay_ip|replay_port
*
* 示例:
* node 5gc.js amf add --name AMF_TEST --project XW_S5GC_1 --sbi-ip 10.0.0.1
* node 5gc.js gnb add --name GNB_TEST --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100
* node 5gc.js ue add --name UE_001 --imsi 460001234567890 --msisdn 8613888888888
* node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613888888888
* node 5gc.js ue edit --id 10337 --set-msisdn 8613888888888
* node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 200.200.200.200
* node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 10.0.0.5
* node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
* node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --precedence 50
* node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_new --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000
* node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED
* node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
* node 5gc.js smpolicy default-add-pcc --project XW_SUPF_5_1_2_4 --pcc-id pcc_new
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const SCRIPTS_DIR = __dirname;
const argv = process.argv.slice(2);
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
printHelp();
process.exit(0);
}
const entity = argv[0].toLowerCase();
const action = (argv[1] || '').toLowerCase();
const VALID_ENTITIES = ['amf', 'udm', 'smf', 'upf', 'gnb', 'ue', 'pcf', 'pcc', 'nrf', 'qos', 'tc', 'smpolicy'];
const VALID_ACTIONS = ['add', 'edit', 'default-rule-add', 'add-pcc', 'ue-add', 'ue-edit', 'dnn-add', 'dnn-edit'];
if (!VALID_ENTITIES.includes(entity)) {
console.error(`\n❌ 未知网元类型: entity`);
console.error(' 可用: ' + VALID_ENTITIES.join(', '));
process.exit(1);
}
if (!action || !VALID_ACTIONS.includes(action)) {
console.error(`\n❌ 未知操作: action || '(空)'`);
console.error(' 用法: node 5gc.js <entity> <action> [options]');
console.error(' 示例: node 5gc.js amf add --help');
process.exit(1);
}
// 子脚本映射
// 所有 edit 均映射到 edit 脚本(单条 + 批量二合一)
const scriptMap = {
'amf:add': 'amf-add-skill.js',
'amf:edit': 'amf-edit-skill.js',
'udm:add': 'ausf-udm-add-skill.js',
'udm:edit': 'ausf-udm-edit-skill.js',
'smf:add': 'smf-pgwc-add-skill.js',
'smf:edit': 'smf-pgwc-edit-skill.js',
'upf:add': 'upf-add-skill.js',
'upf:edit': 'upf-edit-skill.js',
'gnb:add': 'gnb-add-skill.js',
'gnb:edit': 'gnb-edit-skill.js',
'ue:add': 'ue-add-skill.js',
'ue:edit': 'ue-edit-skill.js',
'pcf:add': 'pcf-add-skill.js',
'pcf:edit': 'pcf-edit-skill.js',
'pcf:default-rule-add': 'default-rule-add-skill.js',
'pcc:add': 'pcc-add-skill.js',
'pcc:edit': 'pcc-edit-skill.js',
'nrf:add': 'nrf-add-skill.js',
'nrf:edit': 'nrf-edit-skill.js',
'qos:add': 'qos-add-skill.js',
'tc:add': 'tc-add-skill.js',
'smpolicy:add-pcc': 'smpolicy_add_pcc.js',
'smpolicy:ue-add': 'smpolicy-ue-add-skill.js',
'smpolicy:ue-edit': 'smpolicy-ue-edit-skill.js',
'smpolicy:dnn-add': 'smpolicy-dnn-add-skill.js',
'smpolicy:dnn-edit': 'smpolicy-dnn-edit-skill.js',
};
const scriptFile = scriptMap[`entity:action`];
const scriptPath = path.join(SCRIPTS_DIR, scriptFile);
if (!fs.existsSync(scriptPath)) {
console.error(`\n❌ 脚本不存在: scriptPath`);
process.exit(1);
}
function normalizeChildArgs(entity, action, args) {
const out = [];
let positionalName = null;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const next = i + 1 < args.length ? args[i + 1] : undefined;
if (arg === '--name' && next !== undefined) {
if (entity === 'ue' && action === 'add') {
out.push('--name', next);
} else {
positionalName = next;
}
i++;
continue;
}
if ((entity === 'smf' || entity === 'upf' || entity === 'gnb') && arg === '--pfcp-ip' && next !== undefined) {
out.push('--pfcp_sip', next); i++; continue;
}
if (entity === 'smf' && arg === '--n3-ip' && next !== undefined) {
out.push('--http2_sip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n4-ip' && next !== undefined) {
out.push('--n4_ip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n3-ip' && next !== undefined) {
out.push('--n3_ip', next); i++; continue;
}
if (entity === 'upf' && arg === '--n6-ip' && next !== undefined) {
out.push('--n6_ip', next); i++; continue;
}
if (entity === 'gnb' && arg === '--ngap-ip' && next !== undefined) {
out.push('--ngap_sip', next); i++; continue;
}
if (entity === 'gnb' && arg === '--user-sip-ip-v4' && next !== undefined) {
out.push('--user_sip_ip_v4', next); i++; continue;
}
if (entity === 'gnb' && arg === '--node-id' && next !== undefined) {
out.push('--node_id', next); i++; continue;
}
if (entity === 'amf' && action === 'add') {
if (arg === '--sbi-ip' && next !== undefined) { out.push('--http2_sip', next); i++; continue; }
if (arg === '--sst' && next !== undefined) { i++; continue; }
if (arg === '--sd' && next !== undefined) { i++; continue; }
}
if (entity === 'udm' && action === 'add') {
if (arg === '--auth-supi' && next !== undefined) { i++; continue; }
if (arg === '--auth-op-type' && next !== undefined) { i++; continue; }
if (arg === '--opc' && next !== undefined) { out.push('--op_opc', next); i++; continue; }
}
out.push(arg);
}
if (positionalName) out.unshift(positionalName);
return out;
}
// 去掉 entity 和 action 后的参数传给子脚本
const childArgv = normalizeChildArgs(entity, action, argv.slice(2));
console.log(`\n▶ 5GC entity.toUpperCase() action`);
console.log(' → node ' + scriptFile + ' ' + childArgv.join(' ') + '\n');
// 用子进程调用,保持 CLI 参数隔离
const child = spawn('node', [scriptPath, ...childArgv], {
stdio: 'inherit',
shell: true,
cwd: SCRIPTS_DIR,
});
child.on('exit', (code) => process.exit(code || 0));
child.on('error', (err) => { console.error('启动失败:', err.message); process.exit(1); });
function printHelp() {
console.log(`
5GC Web 仪表自动化 - 统一 CLI
=============================
用法:
node 5gc.js <entity> <action> [options]
网元类型 (entity):
amf - AMF(接入与移动性管理功能)
udm - UDM/AUSF(统一数据管理/认证服务器功能)
smf - SMF/PGW-C(会话管理功能/PDN 连接网关控制面)
upf - UPF/PGW-U(用户面功能/PDN 连接网关用户面)
gnb - gNodeB(5G 基站)
ue - UE(用户终端)
pcf - PCF/PCRF(策略控制功能)
nrf - NRF(网络存储功能)
qos - QoS 模板
tc - Traffic Control 流量控制规则
smpolicy - Smpolicy(会话策略规则)
操作 (action):
add - 添加网元实例
edit - 编辑网元(单个或批量)
default-rule-add - 一键配置完整 PCF 默认规则链路(QoS → TC → PCC → sm_policy_default → PCF)
通用选项:
--url <地址> 5GC 仪表地址(默认 https://192.168.3.89)
--project <工程> 目标工程名称
--name <名称> 网元名称
--id <id> 网元 ID(edit 模式)
--headed 以有头模式运行(显示浏览器)
--help 显示本帮助
字段修改(edit 模式 --set-<field> <value>):
AMF: name|sbi_ip|sbi_port|amf_name|guami|mcc|mnc|sst|sd|ap1|ap2|ap3|ap4|ap5
UDM: name|auth_supi|auth_op_type|op_opc|aud_method|scheme|id|priority
SMF: name|pfcp_ip|n3_ip|n6_ip|dnn|snssai|sliceamba_type
UPF: name|n4_ip|n3_ip|n6_ip|dnn|snssai|count|static_arp|ue_ip_pool
GNB: name|ngap_ip|user_sip_ip_v4|mcc|mnc|stac|etac|node_id|cell_count|replay_ip|replay_port
UE: name|count|mcc|mnc|s_imsi|key|opc|imeisv|msisdn|user_sip_ip_v4|user_sip_ip_v6|replay_ip|replay_port
PCF: http2_sip|http2_port|mcc|mnc
PCF默认规则: --pcf-name <名称> --qos-id <ID> --tc-id <ID> --pcc-id <ID> --precedence <值>
添加示例:
node 5gc.js amf add --name AMF_TEST --project XW_S5GC_1 --sbi-ip 10.0.0.1 --mcc 460 --mnc 01
node 5gc.js gnb add --name GNB_TEST --project XW_S5GC_1 --count 1 --mcc 460 --mnc 01 --stac 1 --etac 100
node 5gc.js ue add --name UE_001 --imsi 460001234567890 --msisdn 8613888888888
node 5gc.js smf add --name SMF_TEST --project XW_S5GC_1 --pfcp-ip 10.0.0.2
node 5gc.js upf add --name UPF_TEST --project XW_S5GC_1 --n4-ip 10.0.0.3
node 5gc.js qos add --project XW_SUPF_5_1_2_4 --qos-id qos_new --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000
node 5gc.js tc add --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED
node 5gc.js pcc add --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos1 --tc tc1
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc
node 5gc.js pcf default-rule-add --project XW_SUPF_5_1_2_4 --pcf-name pcc --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --precedence 50
编辑示例:
node 5gc.js ue edit --project XW_S5GC_1 --set-msisdn 8613888888888
node 5gc.js ue edit --id 10337 --set-msisdn 8613888888888
node 5gc.js gnb edit --project XW_S5GC_1 --set-user_sip_ip_v4 200.200.200.200
node 5gc.js upf edit --project XW_S5GC_1 --set-n4_ip 10.0.0.5
`);
}
FILE:scripts/amf-add-skill.js
#!/usr/bin/env node
/**
* AMF 添加脚本 - 完整修复版
* 功能:登录状态缓存 + .projectSelect 选工程 + evaluate 填写表单 + 算法全勾选 + NSSAI
*/
const { chromium } = require('playwright');
let globalBaseUrl = 'https://192.168.3.89';
const fs = require('fs');
const path = require('path');
// 配置
const CONFIG = {
urls: {
login: '/login',
amfEdit: '/sim_5gc/amf/edit',
amfManagement: '/sim_5gc/amf/index'
},
credentials: {
email: '[email protected]',
password: 'dotouch'
},
sessionDir: path.join(__dirname, '.sessions'),
getSessionFile() {
const host = globalBaseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
};
// 会话管理
class SessionManager {
constructor() {
this.sessionPath = path.join(CONFIG.sessionDir, CONFIG.getSessionFile());
}
async saveSession(context) {
try {
const storageState = await context.storageState();
fs.writeFileSync(this.sessionPath, JSON.stringify({ storageState }, null, 2));
return true;
} catch {
return false;
}
}
async loadSession(browser) {
try {
if (!fs.existsSync(this.sessionPath)) return null;
const { storageState } = JSON.parse(fs.readFileSync(this.sessionPath, 'utf8'));
return await browser.newContext({ storageState, ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
} catch {
return null;
}
}
}
// 算法配置:直接点击 layui 复选框的可见元素
async function configureAlgorithmsSuccess(page) {
await page.waitForSelector('.layui-form-checkbox', { timeout: 5000 });
await page.waitForTimeout(300);
const checkboxCount = await page.locator('.layui-form-checkbox').count();
console.log(` 算法复选框数量: checkboxCount`);
for (let i = 0; i < Math.min(checkboxCount, 8); i++) {
await page.locator('.layui-form-checkbox').nth(i).click();
await page.waitForTimeout(80);
}
const priorities = [
'ea[NEA0]', 'ea[128-NEA1]', 'ea[128-NEA2]', 'ea[128-NEA3]',
'ia[NIA0]', 'ia[128-NIA1]', 'ia[128-NIA2]', 'ia[128-NIA3]'
];
const vals = ['1', '2', '3', '4', '1', '2', '3', '4'];
for (let i = 0; i < priorities.length; i++) {
const inp = page.locator(`input[name="priorities[i]"]`);
if (await inp.count() > 0) {
await inp.fill(vals[i]);
}
}
console.log(` ✅ 算法配置完成`);
}
// 工程选择(精确匹配,分页遍历)
async function selectProject(page, projectName, forceSwitch = true) {
if (!forceSwitch) {
console.log(` 🔧 保持当前工程(用户未指定工程)`);
return true;
}
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.evaluate(() => {
const inputs = document.querySelectorAll('input[type="text"], input[name="name"]');
for (const inp of inputs) { inp.value = ''; }
});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) {
icon.click();
return true;
}
}
}
return false;
}, projectName);
if (clicked) {
await page.waitForTimeout(2000);
return true;
}
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try {
REPLACED
} catch (e) {
break;
}
}
console.log(` ❌ 未找到工程 "projectName"(精确匹配)`);
return false;
}
// 添加 AMF 主流程
async function addAmf(amfName, projectName, explicitProject = true, amfConfig = {}) {
const startTime = Date.now();
const sessionManager = new SessionManager();
const defaultConfig = {
mcc: '460', mnc: '01', region_id: '1', set_id: '1', pointer: '1',
ngap_sip: '200.20.20.1', ngap_port: '38412',
http2_sip: '200.20.20.5', http2_port: '8080',
stac: '101', etac: '102'
};
const cfg = { ...defaultConfig, ...amfConfig };
let browser = null;
try {
browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
let context = await sessionManager.loadSession(browser);
let needLogin = true;
if (context) {
const testPage = await context.newPage();
await testPage.goto(`globalBaseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
if (!testPage.url().includes('/login')) {
needLogin = false;
}
await testPage.close();
}
if (needLogin) {
context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
}
const page = await context.newPage();
if (needLogin) {
await page.goto(`globalBaseUrlCONFIG.urls.login`, { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill(CONFIG.credentials.email);
await page.getByRole('textbox', { name: '密码' }).fill(CONFIG.credentials.password);
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle', { timeout: 10000 });
await sessionManager.saveSession(context);
}
// 选择工程(仅当用户显式指定工程时才切换)
if (!(await selectProject(page, projectName, explicitProject))) {
throw new Error(`工程 "projectName" 不存在或无法选中`);
}
// 进入编辑页面
await page.goto(`globalBaseUrlCONFIG.urls.amfEdit`, { waitUntil: 'networkidle', timeout: 15000 });
if (!page.url().includes('/amf/edit')) {
await page.goto(`globalBaseUrl/sim_5gc/amf/edit`);
await page.waitForSelector('input[name="name"]', { timeout: 10000 });
}
// 通过 evaluate 直接填写表单
await page.evaluate(({ amfName, cfg }) => {
const set = (name, value) => {
const el = document.querySelector(`input[name="name"]`);
if (el) {
el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
}
};
set('name', amfName);
set('mcc', cfg.mcc);
set('mnc', cfg.mnc);
set('region_id', cfg.region_id);
set('set_id', cfg.set_id);
set('pointer', cfg.pointer);
set('ngap_sip', cfg.ngap_sip);
set('ngap_port', cfg.ngap_port);
set('http2_sip', cfg.http2_sip);
set('http2_port', cfg.http2_port);
set('stac', cfg.stac);
set('etac', cfg.etac);
}, { amfName, cfg });
// 类型选择:仿真设备
await page.locator('.layui-unselect').first().click();
await page.waitForTimeout(300);
await page.locator('dd').filter({ hasText: '仿真设备' }).click();
// 配置算法
await configureAlgorithmsSuccess(page);
// 配置 NSSAI
await page.getByRole('row', { name: /数量.*nssai/ }).getByRole('button').click();
await page.waitForTimeout(500);
await page.locator('input[name="config[count][]"]').fill('1');
await page.getByRole('row', { name: /nssai.*添加.*删除/ }).locator('span').click();
await page.waitForTimeout(800);
const iframeEl = page.locator('iframe[name="layui-layer-iframe2"]');
const iframe = await iframeEl.contentFrame({ timeout: 5000 });
await iframe.getByRole('row', { name: /\*.*SST.*SD/ }).getByRole('button').click();
await iframe.locator('input[name="nssai[snssai_sst][]"]').fill('1');
await iframe.locator('input[name="nssai[snssai_sd][]"]').fill('111111');
await iframe.getByRole('button', { name: '提交' }).click();
await page.waitForTimeout(800);
// 提交表单
await page.getByRole('button', { name: '提交' }).click();
// 等待页面跳转到 AMF 列表页面,若未跳转则强制跳转
try {
await page.waitForURL(`**/amf/index`, { timeout: 8000 });
} catch (e) {
await page.goto(`globalBaseUrlCONFIG.urls.amfManagement`, { waitUntil: 'networkidle', timeout: 15000 });
}
await page.waitForTimeout(2000);
// 验证结果:只要页面成功跳转到 AMF 列表页,即认为添加成功
let found = false;
const finalUrl = page.url();
if (finalUrl.includes('/amf/index')) {
console.log(` ✅ 页面已跳转至 AMF 列表: finalUrl`);
found = true;
}
await browser.close();
const totalTime = (Date.now() - startTime) / 1000;
if (found) {
return { success: true, amfName, totalTime };
} else {
return { success: false, amfName, totalTime };
}
} catch (err) {
if (browser) await browser.close();
throw err;
}
}
// 主函数
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('用法: node amf-add-skill.js <AMF名称> [--project <工程名>] [--url <地址>] [--mcc 460] [...]');
process.exit(1);
}
let amfName = null;
let projectName = '5G_basic_process';
let amfConfig = {};
let explicitProject = false;
for (let i = 0; i < args.length; i++) {
if (!args[i].startsWith('-')) {
amfName = args[i];
} else if (args[i] === '--project' || args[i] === '-p') {
projectName = args[++i];
explicitProject = true;
} else if (args[i] === '--url') {
let u = args[++i];
if (u && !u.startsWith('http')) u = 'https://' + u;
globalBaseUrl = u;
} else if (args[i].startsWith('--')) {
amfConfig[args[i].substring(2)] = args[++i];
}
}
if (!amfName) {
console.error('错误: 请指定 AMF 名称');
process.exit(1);
}
console.log(`AMF: amfName | 工程: projectName | 地址: globalBaseUrl`);
try {
const result = await addAmf(amfName, projectName, explicitProject, amfConfig);
console.log(result.success
? `成功! AMF "result.amfName" 添加完成 (result.totalTime.toFixed(2)s)`
: `失败! 未找到 AMF "result.amfName"`);
process.exit(result.success ? 0 : 1);
} catch (err) {
console.error(`执行异常: err.message`);
process.exit(1);
}
}
main();
FILE:scripts/default-rule-add-skill.js
/**
* default-rule-add-skill.js - PCF 默认规则一键添加工具
*
* 完整链路(一次性完成):
* 1. 创建 QoS 模板(自动选5qi)
* 2. 创建 Traffic Control(ENABLED)
* 3. 创建 PCC 规则(绑定 qos + tc)
* 4. 创建/更新 sm_policy_default(绑定 pcc)
* 5. PCF default_smpolicy → sm_policy_default
*
* 用法:
* node default-rule-add-skill.js --project XW_SUPF_5_1_2_4 --headed
* node default-rule-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos1 --tc-id tc1 --pcc-id pcc_default --headed
*
* 参数(均有默认值,可全部省略):
* --project 工程名(默认 XW_S5GC_1)
* --pcf-name PCF实例名称(必填,如 qqq)
* --qos-id QoS模板ID(默认自动生成 qos_default_{timestamp})
* --5qi 5QI值(不指定则自动选择未使用的值)
* --maxbr-ul 上行最大比特率(默认 10000000)
* --maxbr-dl 下行最大比特率(默认 20000000)
* --gbr-ul 上行保证比特率(默认 5000000)
* --gbr-dl 下行保证比特率(默认 5000000)
* --tc-id TC规则ID(默认自动生成 tc_default_{timestamp})
* --flow-status TC流状态(默认 ENABLED)
* --pcc-id PCC规则ID(默认 pcc_default)
* --precedence PCC优先级(默认 63)
* --headed 显示浏览器窗口
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const ts = Date.now();
const opts = {
project: 'XW_S5GC_1',
pcfName: null, // null = 使用 pccId 作为 PCF 名称(向后兼容)
// QoS 参数
qosId: null, // null = 自动生成
qi: null,
maxbrUl: '10000000',
maxbrDl: '20000000',
gbrUl: '5000000',
gbrDl: '5000000',
// TC 参数
tcId: null, // null = 自动生成
flowStatus: 'ENABLED',
// PCC 参数
pccId: null, // null = 自动生成
precedence: '63',
// PCF 参数(网元名称)
pcfName: null, // 若未提供则使用 pccId 作为默认名称
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--qos-id') opts.qosId = args[++i];
else if (args[i] === '--5qi') opts.qi = args[++i];
else if (args[i] === '--maxbr-ul') opts.maxbrUl = args[++i];
else if (args[i] === '--maxbr-dl') opts.maxbrDl = args[++i];
else if (args[i] === '--gbr-ul') opts.gbrUl = args[++i];
else if (args[i] === '--gbr-dl') opts.gbrDl = args[++i];
else if (args[i] === '--tc-id') opts.tcId = args[++i];
else if (args[i] === '--flow-status') opts.flowStatus = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--precedence') opts.precedence = args[++i];
else if (args[i] === '--pcf-name') opts.pcfName = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
// 自动生成ID(如果未指定)
if (!opts.qosId) opts.qosId = `qos_default_ts`;
if (!opts.tcId) opts.tcId = `tc_default_ts`;
if (!opts.pccId) opts.pccId = `pcc_default`;
// 如果未提供 PCF 名称,默认使用 PCC ID(与业务保持一致)
if (!opts.pcfName) opts.pcfName = opts.pccId;
return opts;
}
// ─── 通用工具 ────────────────────────────────────────────────────────────
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let clicked = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
clicked = true;
}
});
return clicked;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
async function goto(page, url) {
await page.goto(`globalBaseUrlurl`, { waitUntil: 'load', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
}
// ─── Step 1: 创建 QoS 模板 ──────────────────────────────────────────────
async function getUsedQis(page) {
await goto(page, '/sim_5gc/predfPolicy/qos/index');
return await page.evaluate(() => {
const qis = [];
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[3].textContent.trim()) {
qis.push(parseInt(cells[3].textContent.trim()));
}
});
return qis;
});
}
async function addQos(page, opts) {
// 自动选 5qi(先获取已用列表)
if (!opts.qi) {
await goto(page, '/sim_5gc/predfPolicy/qos/index');
const used = await page.evaluate(() => {
const qis = [];
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[3].textContent.trim()) {
qis.push(parseInt(cells[3].textContent.trim()));
}
});
return qis;
});
const candidates = [8, 9, 6, 5, 7, 4, 3, 2, 1];
for (const c of candidates) { if (!used.includes(c)) { opts.qi = String(c); break; } }
if (!opts.qi) opts.qi = String(used[0] + 1);
console.log(` i️ 已用5qi: used.join(','),自动选择 opts.qi`);
}
await goto(page, '/sim_5gc/predfPolicy/qos/index');
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 10000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(500);
await frame.locator('input[name="qosId"]').fill(opts.qosId);
await frame.locator('input[name="5qi"]').fill(opts.qi);
await frame.locator('input[name="maxbrUl"]').fill(opts.maxbrUl);
await frame.locator('input[name="maxbrDl"]').fill(opts.maxbrDl);
await frame.locator('input[name="gbrUl"]').fill(opts.gbrUl);
await frame.locator('input[name="gbrDl"]').fill(opts.gbrDl);
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ QoS模板 opts.qosId 已创建 (5qi=opts.qi)`);
}
// ─── Step 2: 创建 TC ────────────────────────────────────────────────────
async function addTc(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/trafficCtl/index');
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 等待 iframe 出现在 DOM 中
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 10000 });
const frame = page.frame('layui-layer-iframe2');
await frame.waitForLoadState('domcontentloaded');
// 等待 tcId input 出现
await frame.waitForSelector('input[name="tcId"]', { timeout: 10000 });
await page.waitForTimeout(500);
await frame.locator('input[name="tcId"]').fill(opts.tcId);
// 等待 select[name="flowStatus"] 出现在 DOM 中
const sel = frame.locator('select[name="flowStatus"]');
try {
await sel.waitFor({ state: 'attached', timeout: 5000 });
await sel.selectOption(opts.flowStatus, { force: true });
console.log(` flowStatus = opts.flowStatus`);
} catch(e) {
// 如果 select 不存在(如没有 flowStatus 字段),跳过
console.log(` i️ flowStatus select 不存在,跳过`);
}
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ TC opts.tcId 已创建 (flowStatus=opts.flowStatus)`);
}
// ─── Step 3: 创建 PCC ────────────────────────────────────────────────────
async function addPcc(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/pcc/index');
// 检查是否已存在同名 PCC,存在则先删除
const existingId = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[2].textContent.trim() === targetId) {
return cells[1].textContent.trim(); // 返回数字ID
}
}
return null;
}, opts.pccId);
if (existingId) {
// 删除旧记录
await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 10 && cells[1].textContent.trim() === id) {
const links = cells[9].querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '删除') { l.click(); return; } }
}
}
}, existingId);
await page.waitForTimeout(1500);
// 处理删除确认对话框
const confirmBtn = page.locator('.layui-layer-dialog .layui-layer-btn0, .layui-layer-btn a:first-child');
if (await confirmBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await confirmBtn.click();
await page.waitForTimeout(2000);
}
// 确保遮罩关闭
await page.keyboard.press('Escape');
await page.waitForTimeout(1000);
console.log(` 🗑️ 已删除旧 PCC opts.pccId,准备重建`);
}
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
await page.locator('input[name="pccRuleId"]').fill(opts.pccId);
await page.locator('input[name="precedence"]').fill(opts.precedence);
// xm-select[0] = refQosData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[0].parentElement.click());
await page.waitForTimeout(1000);
const qosOpt = page.locator('.xm-option.show-icon', { hasText: opts.qosId });
if (await qosOpt.isVisible({ timeout: 3000 }).catch(() => false)) await qosOpt.click();
await page.waitForTimeout(500);
// xm-select[1] = refTcData
await page.evaluate(() => document.querySelectorAll('input.xm-select-default')[1].parentElement.click());
await page.waitForTimeout(1000);
const tcOpt = page.locator('.xm-option.show-icon', { hasText: opts.tcId });
if (await tcOpt.isVisible({ timeout: 3000 }).catch(() => false)) await tcOpt.click();
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(` ✅ PCC规则 opts.pccId 已创建 (refQosData=opts.qosId, refTcData=opts.tcId)`);
}
// ─── Step 4: 创建/更新 sm_policy_default(使用正确的 form_data 格式)────────────
async function addOrUpdateSmpolicy(page, pccId) {
console.log(`\n=== Step 4: 创建/更新 sm_policy_default (pccRules=pccId) ===`);
// 1. 进入 PCF,选中 pccId 行,点击 smpolicy 按钮
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const cb = row.querySelector('input[type="checkbox"]');
if (cb) cb.click();
}
}
}, pccId);
await page.waitForTimeout(500);
await page.locator('button:has-text("smpolicy")').click({ force: true });
await page.waitForTimeout(3000);
console.log(' smpolicy 页面 URL:', page.url());
// 2. 获取 CSRF token(从页面)
const token = await page.evaluate(() => document.querySelector('input[name="_token"]')?.value || '');
if (!token) {
console.error(' ❌ 未找到 _token');
return false;
}
console.log(' _token: ...' + token.substring(0, 10) + '...');
// 3. 检查是否已有 sm_policy_default
const existing = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
return { id: cells[1].textContent.trim(), pccRules: cells[4].textContent.trim() };
}
}
return null;
});
if (!existing) {
console.log(' ℹ️ sm_policy_default 不存在,正在创建...');
await page.locator('button:has-text("添加")').click({ force: true });
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 构造正确的 form_data JSON
const formDataJson = JSON.stringify({
name: 'sm_policy_default',
pccRules: [pccId],
reflectiveQoSTimer: 86400
});
const params = new URLSearchParams();
params.append('_token', token);
params.append('form_data', formDataJson);
console.log(' form_data:', formDataJson);
const resp = await frm.evaluate(async (args) => {
const { tok, bodyStr } = args;
try {
const r = await fetch('/sim_5gc/smpolicy/default/edit', {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': tok,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { tok: token, bodyStr: params.toString() });
console.log(' 创建响应:', resp.status);
if (resp.status >= 400) {
try { console.log(' 错误:', JSON.stringify(JSON.parse(resp.body))); }
catch { console.log(' 响应:', resp.body?.substring(0, 200)); }
return false;
} else {
console.log(' ✅ 创建成功!响应:', resp.body?.substring(0, 200));
return true;
}
} else {
console.log(' ✅ sm_policy_default 已存在 (id=' + existing.id + ', pccRules=' + existing.pccRules + ')');
if (existing.pccRules.includes(pccId)) {
console.log(' ✅ pccRules 已包含 ' + pccId);
return true;
} else {
console.log(' ℹ️ 更新 sm_policy_default,添加 pccRules=' + pccId + '...');
// 点击编辑
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return; } }
}
}
});
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 获取当前 pccRules
const currentPcc = await frm.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs.length > 1 ? inputs[1].value : '';
});
const existingRules = currentPcc ? currentPcc.split(',').filter(Boolean) : [];
if (!existingRules.includes(pccId)) existingRules.push(pccId);
const recId = await frm.evaluate(() => {
const el = document.querySelector('input[name="id"]');
return el ? el.value : '';
});
// 更新用的 form_data
const formDataJson = JSON.stringify({
name: 'sm_policy_default',
pccRules: existingRules,
reflectiveQoSTimer: 86400,
id: recId
});
const params = new URLSearchParams();
params.append('_token', token);
params.append('form_data', formDataJson);
const resp = await frm.evaluate(async (args) => {
const { tok, bodyStr, recId } = args;
try {
const r = await fetch('/sim_5gc/smpolicy/default/edit/' + recId, {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': tok,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { tok: token, bodyStr: params.toString(), recId });
console.log(' 更新响应:', resp.status);
if (resp.status >= 400) {
try { console.log(' 错误:', JSON.stringify(JSON.parse(resp.body))); }
catch { console.log(' 响应:', resp.body?.substring(0, 200)); }
return false;
} else {
console.log(' ✅ 更新成功!响应:', resp.body?.substring(0, 200));
return true;
}
}
}
}// ─── Step 5: PCF default_smpolicy ────────────────────────────────────────
// 正确流程(根据 UI 调试结果):
// 1. 在 PCF 列表先选中 qqq 行(单击,不要点编辑)
// 2. 再点击工具栏 "smpolicy" 按钮 → 页面加载 sm_policy_default 表单(带 qqq 上下文)
// 3. 创建 sm_policy_default(此时 name 应为 qqq 关联的默认策略名)
// 4. 保存后返回 PCF 编辑弹窗 → default_smpolicy 下拉有数据 → 选择 → 提交
async function setPcfDefaultSmpolicy(page, pcfName) {
console.log(`\n=== Step 5: 配置 PCF "pcfName" default_smpolicy ===`);
// 1. 进入 PCF 列表,点击指定 PCF 的编辑按钮
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return true; } }
}
}
return false;
}, pcfName);
if (!clicked) {
console.error(` ❌ 未找到 PCF "pcfName" 的编辑按钮`);
return false;
}
await page.waitForTimeout(3000);
await page.waitForSelector('iframe[name="layui-layer-iframe2"]', { timeout: 15000 });
const frm = page.frame('layui-layer-iframe2');
await frm.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
// 2. 获取 token 和 PCF ID
const token = await frm.evaluate(() => {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
});
if (!token) {
console.error(' ❌ 未找到 CSRF token (meta[name="csrf-token"])');
return false;
}
console.log(` Token: ...token.substring(0, 10)... (from meta tag)`);
const pcfId = await frm.evaluate(() => document.querySelector('input[name="id"]')?.value || '');
if (!pcfId) {
console.error(' ❌ 未找到 PCF ID');
return false;
}
console.log(` PCF ID: pcfId`);
// 3. 获取当前表单数据
const formData = await frm.evaluate(() => {
const form = document.querySelector('form');
if (!form) return {};
const data = new FormData(form);
const entries = {};
data.forEach((v, k) => { entries[k] = v; });
return entries;
});
// 4. 获取 sm_policy_default 的 ID - 通过主页面,不关闭弹窗
console.log(' 获取 sm_policy_default 的 ID...');
// 在主页面(不是 iframe)中打开新标签页查看 smpolicy 列表
const smpId = await page.evaluate(async () => {
// 在新窗口中打开 smpolicy 页面
const newWindow = window.open('/sim_5gc/smpolicy/default/index', '_blank');
if (!newWindow) return '';
// 等待新窗口加载
await new Promise(resolve => setTimeout(resolve, 2000));
// 从新窗口获取数据
const rows = newWindow.document.querySelectorAll('.layui-table tbody tr');
let foundId = '';
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === 'sm_policy_default') {
foundId = cells[1].textContent.trim();
break;
}
}
// 关闭新窗口
newWindow.close();
return foundId;
});
if (!smpId) {
console.error(' ❌ 未找到 sm_policy_default 的 ID');
// 尝试使用已知的 ID(如果之前创建过)
console.log(' ℹ️ 尝试使用默认 ID 9771');
return '9771'; // 返回默认 ID,让调用者决定
}
console.log(` sm_policy_default ID: smpId`);
// 5. 构造更新数据 - 设置 default_smpolicy 为 sm_policy_default 的 ID
const updateData = {
...formData,
'assoc_smpolicy[default_smpolicy]': smpId, // 使用 ID 而不是名称
'_token': token
};
// 移除空值(除了 select 字段)
Object.keys(updateData).forEach(key => {
if (updateData[key] === '' && !key.includes('select') && key !== 'assoc_smpolicy[default_smpolicy]') {
delete updateData[key];
}
});
// 6. 发送 POST 请求
const params = new URLSearchParams();
Object.entries(updateData).forEach(([k, v]) => {
params.append(k, v);
});
console.log(` 提交数据: assoc_smpolicy[default_smpolicy]=updateData['assoc_smpolicy[default_smpolicy]']`);
const resp = await frm.evaluate(async (args) => {
const { pcfId, bodyStr, token } = args;
try {
const r = await fetch(`/sim_5gc/pcf/edit/pcfId`, {
method: 'POST',
body: bodyStr,
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'x-csrf-token': token,
'x-requested-with': 'XMLHttpRequest'
}
});
const text = await r.text();
return { status: r.status, body: text };
} catch(e) {
return { error: e.message };
}
}, { pcfId, bodyStr: params.toString(), token });
console.log(` 响应状态: resp.status`);
if (resp.status >= 400) {
try { console.log(` 错误: JSON.stringify(JSON.parse(resp.body))`); }
catch { console.log(` 响应: resp.body?.substring(0, 200)`); }
return false;
} else {
console.log(` ✅ PCF "pcfName" default_smpolicy 设置成功!响应: resp.body?.substring(0, 100)`);
return true;
}
}async function verify(page, opts) {
await goto(page, '/sim_5gc/predfPolicy/pcc/index');
const pcc = await page.evaluate((id) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === id) {
return { pccRuleId: cells[2].textContent.trim(), precedence: cells[4].textContent.trim(), refQosData: cells[5].textContent.trim(), refTcData: cells[6].textContent.trim() };
}
}
return null;
}, opts.pccId);
await goto(page, '/sim_5gc/smpolicy/default/index');
const smp = await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6 && cells[2].textContent.trim() === 'sm_policy_default') return { pccRules: cells[4].textContent.trim() };
}
return null;
});
await goto(page, '/sim_5gc/pcf/index');
await page.waitForTimeout(3000);
// 点击指定的 PCF 编辑按钮
const pcfName = opts.pcfName || opts.pccId;
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const links = row.querySelectorAll('a');
for (const l of links) { if (l.textContent.trim() === '编辑') { l.click(); return true; } }
}
}
return false;
}, pcfName);
if (!clicked) {
console.log(' ⚠️ 未找到 PCF "' + pcfName + '" 的编辑按钮,使用第一个 PCF');
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) rows[0].querySelector('a')?.click();
});
}
await page.waitForTimeout(3000); const frame = page.frame('layui-layer-iframe2');
const pcfSmp = frame ? await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[0]?.parentElement?.textContent?.match(/[\w_]+/g)?.[0] || '';
}) : '';
console.log('\n========================================');
console.log('验证结果');
console.log('========================================');
const tests = [
{ name: `PCC opts.pccId 存在`, pass: !!pcc },
{ name: `refQosData = opts.qosId`, pass: pcc?.refQosData === opts.qosId },
{ name: `refTcData = opts.tcId`, pass: pcc?.refTcData === opts.tcId },
{ name: `sm_policy_default 包含 opts.pccId`, pass: smp?.pccRules?.includes(opts.pccId) },
{ name: `PCF default_smpolicy = sm_policy_default`, pass: pcfSmp === 'sm_policy_default' },
];
for (const t of tests) console.log(` '❌' t.name`);
if (pcc) console.log(`\n PCC: pccRuleId=pcc.pccRuleId, precedence=pcc.precedence, refQosData=pcc.refQosData, refTcData=pcc.refTcData`);
if (smp) console.log(` smp: pccRules=[smp.pccRules]`);
console.log('========================================');
return tests.every(t => t.pass);
}
// ─── 主流程 ─────────────────────────────────────────────────────────────
async function main() {
const opts = parseArgs();
console.log('\n========================================');
console.log('PCF 默认规则一键配置');
console.log(`工程: opts.project`);
console.log(`QoS: opts.qosId (5qi=opts.qi || '自动')`);
console.log(`TC: opts.tcId (flowStatus=opts.flowStatus)`);
console.log(`PCF: opts.pcfName || opts.pccId`);
console.log(`PCC: opts.pccId (precedence=opts.precedence)`);
console.log('========================================\n');
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
console.log('📦 Step 1: 创建 QoS 模板...');
await addQos(page, opts);
console.log('📦 Step 2: 创建 Traffic Control...');
await addTc(page, opts);
console.log('📦 Step 3: 创建 PCC 规则...');
await addPcc(page, opts);
console.log('📦 Step 4: 更新 sm_policy_default...');
await addOrUpdateSmpolicy(page, opts.pccId);
console.log('📦 Step 5: 配置 PCF default_smpolicy...');
const pcfName = opts.pcfName || opts.pccId; // 向后兼容:如果没有指定 pcf-name,使用 pcc-id
await setPcfDefaultSmpolicy(page, pcfName);
console.log('\n📦 验证...');
const ok = await verify(page, opts);
console.log(ok ? '\n🎉 全部完成!' : '\n⚠️ 部分步骤存在问题,请检查');
await browser.close();
process.exit(ok ? 0 : 1);
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/nrf-add-skill.js
/**
* NRF 添加脚本
* 完整流程:登录 → 选工程 → 进NRF列表 → 点添加(弹窗iframe) → 填表单 → 提交
* 用法: node nrf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \
* [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]
* 示例: node nrf-add-skill.js NRF-TEST --project XW_S5GC_1
*/
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const BASE_URL = 'https://192.168.3.89';
const SESSION_DIR = path.join(__dirname, '.sessions');
function getSessionFile(baseUrl) {
const host = baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
async function login(page, baseUrl) {
const sessionPath = path.join(SESSION_DIR, getSessionFile(baseUrl));
if (fs.existsSync(sessionPath)) {
try {
const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
if (storageState.cookies) {
await page.context().addCookies(storageState.cookies);
await page.goto(baseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {});
if (!page.url().includes('/login')) {
console.log(' ✅ 使用缓存会话');
return true;
}
}
} catch {}
}
await page.goto(baseUrl + '/login', { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
const ctx = page.context();
const storageState = await ctx.storageState();
fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2));
console.log(' ✅ 登录成功');
return true;
}
async function selectProject(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (clicked) { await page.waitForTimeout(2000); return true; }
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try { await nextBtn.click(); } catch { break; }
await page.waitForTimeout(1500);
}
console.log(` ❌ 未找到工程 "projectName"`);
return false;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node nrf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]');
console.log(' [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]');
console.log('示例: node nrf-add-skill.js NRF-TEST --project XW_S5GC_1');
process.exit(1);
}
const name = args[0];
let headless = true;
let project = 'XW_S5GC_1';
let http2_sip = '192.168.20.100';
let http2_port = '80';
let mcc = '460';
let mnc = '01';
for (let i = 1; i < args.length; i++) {
if (args[i] === '--headed') headless = false;
else if (args[i] === '--project') project = args[++i];
else if (args[i] === '--url') BASE_URL = args[++i];
else if (args[i] === '--http2_sip') http2_sip = args[++i];
else if (args[i] === '--http2_port') http2_port = args[++i];
else if (args[i] === '--MCC') mcc = args[++i];
else if (args[i] === '--MNC') mnc = args[++i];
}
console.log(`▶ 添加 NRF: name`);
console.log(` http2_sip=http2_sip http2_port=http2_port MCC=mcc MNC=mnc`);
console.log(` 工程: project`);
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page, BASE_URL);
const ok = await selectProject(page, project);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 NRF 列表(先点"核心网"菜单,再点"NRF")
await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/nrf/"]');
for (const l of links) {
if (l.textContent.trim().includes('NRF')) { l.click(); return; }
}
});
await page.waitForTimeout(3000);
console.log(' ✓ 进入NRF列表,URL:', page.url());
// 点添加按钮
await page.waitForSelector('button:has-text("添加")', { timeout: 10000 }).catch(() => {});
await page.locator('button:has-text("添加")').first().click();
await page.waitForTimeout(2000);
console.log(' ✓ 点添加(弹窗)');
// 切换到弹窗 iframe
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
if (!frame) throw new Error('未找到弹窗 iframe');
await frame.waitForLoadState('domcontentloaded');
console.log(' ✓ 切换到弹窗iframe');
// 名称
await frame.locator('input[name="name"]').fill(name);
console.log(` ✓ name = name`);
// 类型下拉
await frame.getByRole('textbox', { name: '请选择' }).first().click();
await frame.getByRole('definition').filter({ hasText: '仿真设备' }).click();
await page.waitForTimeout(500);
console.log(' ✓ 类型 = 仿真设备');
// MCC
await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc);
console.log(` ✓ MCC = mcc`);
// MNC
await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc);
console.log(` ✓ MNC = mnc`);
// HTTP2 SIP
await frame.locator('input[name="http2_sip"]').fill(http2_sip);
console.log(` ✓ http2_sip = http2_sip`);
// HTTP2 PORT
await frame.locator('input[name="http2_port"]').fill(http2_port);
console.log(` ✓ http2_port = http2_port`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(' ✓ 已提交');
const url = page.url();
if (url.includes('/nrf/index')) {
console.log(` ✅ 添加成功,URL: url`);
} else {
console.log(` ⚠️ 可能未保存,URL: url`);
}
await browser.close();
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
FILE:scripts/pcc-add-skill.js
/**
* pcc-add-skill.js - PCC规则添加工具(修复版)
*
* 用法:
* node pcc-add-skill.js --project XW_SUPF_5_1_2_4 --pcc-id pcc_new --qos qos2 --tc tc1 [--precedence 63] [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --pcc-id 新PCC规则ID(必填,字母/数字/下划线)
* --precedence 优先级(默认 63,用户指定时用指定值)
* --qos QoS模板名称(必填,如 qos1 / qos2)
* --tc 流量控制名称(必填,如 tc1)
* --flow-desc 流描述(可选)
* --headed 显示浏览器窗口
*
* 完整链路:
* 点击"添加" → 主框架跳转 /predfPolicy/pcc/edit
* → 填写 pccRuleId + precedence
* → xm-select 选 qos(第0个)+ tc(第1个)+ 可选chg(第2个)
* → 提交 → 返回列表页
*
* xm-select 交互(Playwright locator):
* 1. JS: inputs[idx].parentElement.click() 打开下拉
* 2. Playwright locator: page.locator('.xm-option.show-icon', {hasText}).click() 选择选项
* 3. page.keyboard.press('Escape') 关闭下拉
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
pccId: null,
precedence: null, // null = 使用默认值63
qos: null, // 必填
tc: null, // 必填
flowDesc: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--precedence') opts.precedence = args[++i];
else if (args[i] === '--qos') opts.qos = args[++i];
else if (args[i] === '--tc') opts.tc = args[++i];
else if (args[i] === '--flow-desc') opts.flowDesc = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.pccId) {
console.error('❌ 缺少 --pcc-id 参数');
process.exit(1);
}
if (!opts.qos) {
console.error('❌ 缺少 --qos 参数(QoS模板名称)');
process.exit(1);
}
if (!opts.tc) {
console.error('❌ 缺少 --tc 参数(流量控制名称)');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
// 先尝试搜索工程名
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) {
// 尝试逐页查找
for (let p = 1; p <= 100; p++) {
const found = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, projectName);
if (found) break;
const hasNext = await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) {
if (l.textContent.trim() === 'Next' && !l.classList.contains('jsgrid-pager-disabled')) return true;
}
return false;
});
if (!hasNext) break;
await page.evaluate(() => {
const links = document.querySelectorAll('.jsgrid-pager a');
for (const l of links) {
if (l.textContent.trim() === 'Next') { l.click(); return; }
}
});
await page.waitForTimeout(2000);
}
}
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 去 PCC 列表页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达PCC列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 等待输入框出现(URL跳转完成)
await page.waitForFunction(() => window.location.href.includes('/predfPolicy/pcc/edit'), { timeout: 10000 });
await page.waitForTimeout(3000);
console.log(`✅ 到达添加页: page.url()`);
// 填写文本字段
const precedence = opts.precedence !== null ? String(opts.precedence) : '63';
await page.locator('input[name="pccRuleId"]').fill(opts.pccId);
await page.locator('input[name="precedence"]').fill(precedence);
console.log(` pccRuleId="opts.pccId", precedence="precedence"'(默认63)'`);
// ── xm-select[0] = refQosData ──────────────────────────────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[0]) inputs[0].parentElement.click();
});
await page.waitForTimeout(1000);
const qosVisible = await page.locator('.xm-option.show-icon', { hasText: opts.qos }).isVisible({ timeout: 3000 }).catch(() => false);
if (qosVisible) {
await page.locator('.xm-option.show-icon', { hasText: opts.qos }).click();
console.log(` ✅ refQosData=opts.qos 已选`);
} else {
console.log(` ❌ refQosData=opts.qos 不可见`);
}
await page.waitForTimeout(500);
// ── xm-select[1] = refTcData ─────────────────────────────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1000);
const tcVisible = await page.locator('.xm-option.show-icon', { hasText: opts.tc }).isVisible({ timeout: 3000 }).catch(() => false);
if (tcVisible) {
await page.locator('.xm-option.show-icon', { hasText: opts.tc }).click();
console.log(` ✅ refTcData=opts.tc 已选`);
} else {
console.log(` ❌ refTcData=opts.tc 不可见`);
}
await page.waitForTimeout(500);
// ── xm-select[2] = refChgData(可选,如有则自动选第一个)────────
await page.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[2]) inputs[2].parentElement.click();
});
await page.waitForTimeout(1000);
const firstChg = page.locator('.xm-option.show-icon').first();
if (await firstChg.isVisible({ timeout: 2000 }).catch(() => false)) {
const txt = await firstChg.textContent();
await firstChg.click();
console.log(` ℹ️ refChgData=(txt.trim()) 已选`);
}
await page.waitForTimeout(500);
// 关闭 xm-select 下拉(按 Escape 避免遮罩拦截提交按钮)
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
// 提交
await page.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ PCC规则 opts.pccId 已提交`);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/pcc/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const pccData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetId) {
return {
pccRuleId: cells[2].textContent.trim(),
precedence: cells[4].textContent.trim(),
refQosData: cells[5].textContent.trim(),
refTcData: cells[6].textContent.trim(),
};
}
}
return null;
}, opts.pccId);
if (pccData) {
console.log('\n📋 验证结果:');
console.log(` pccRuleId = pccData.pccRuleId`);
console.log(` precedence = pccData.precedence`);
console.log(` refQosData = pccData.refQosData '❌'`);
console.log(` refTcData = pccData.refTcData '❌'`);
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/pcf-add-skill.js
/**
* PCF/PCRF 添加脚本
* 完整流程:登录 → 选工程 → 进PCF列表 → 点添加(弹窗iframe) → 填表单 → 提交
* 用法: node pcf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed] \
* [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]
* 示例: node pcf-add-skill.js PCF-TEST --project XW_S5GC_1
*/
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const BASE_URL = 'https://192.168.3.89';
const SESSION_DIR = path.join(__dirname, '.sessions');
function getSessionFile(baseUrl) {
const host = baseUrl.replace(/https?:\/\//, '').replace(/\./g, '_');
return `5gc_session_host.json`;
}
async function login(page, baseUrl) {
const sessionPath = path.join(SESSION_DIR, getSessionFile(baseUrl));
if (fs.existsSync(sessionPath)) {
try {
const storageState = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
if (storageState.cookies) {
await page.context().addCookies(storageState.cookies);
await page.goto(baseUrl + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 8000 }).catch(() => {});
if (!page.url().includes('/login')) {
console.log(' ✅ 使用缓存会话');
return true;
}
}
} catch {}
}
await page.goto(baseUrl + '/login', { waitUntil: 'networkidle', timeout: 15000 });
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForLoadState('networkidle');
const ctx = page.context();
const storageState = await ctx.storageState();
fs.writeFileSync(sessionPath, JSON.stringify({ cookies: storageState.cookies }, null, 2));
console.log(' ✅ 登录成功');
return true;
}
async function selectProject(page, projectName) {
await page.goto(BASE_URL + '/sim_5gc/project/index', { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForSelector('.jsgrid-row, .jsgrid-alt-row', { timeout: 5000 }).catch(() => {});
await page.waitForTimeout(300);
for (let pageNum = 1; pageNum <= 200; pageNum++) {
const clicked = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === targetName) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (clicked) { await page.waitForTimeout(2000); return true; }
const nextBtn = page.locator('.jsgrid-pager a:has-text("Next")');
if (!(await nextBtn.count())) break;
try { await nextBtn.click(); } catch { break; }
await page.waitForTimeout(1500);
}
console.log(` ❌ 未找到工程 "projectName"(精确匹配)`);
return false;
}
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('用法: node pcf-add-skill.js <名称> [--project <工程>] [--url <地址>] [--headed]');
console.log(' [--http2_sip <IP>] [--http2_port <端口>] [--MCC <值>] [--MNC <值>]');
console.log('示例: node pcf-add-skill.js PCF-TEST --project XW_S5GC_1');
process.exit(1);
}
const name = args[0];
let headless = true;
let project = 'XW_S5GC_1';
let http2_sip = '192.168.20.90';
let http2_port = '80';
let mcc = '460';
let mnc = '01';
for (let i = 1; i < args.length; i++) {
if (args[i] === '--headed') headless = false;
else if (args[i] === '--project') project = args[++i];
else if (args[i] === '--url') BASE_URL = args[++i];
else if (args[i] === '--http2_sip') http2_sip = args[++i];
else if (args[i] === '--http2_port') http2_port = args[++i];
else if (args[i] === '--MCC') mcc = args[++i];
else if (args[i] === '--MNC') mnc = args[++i];
}
console.log(`▶ 添加 PCF: name`);
console.log(` http2_sip=http2_sip http2_port=http2_port MCC=mcc MNC=mnc`);
console.log(` 工程: project`);
const browser = await chromium.launch({ headless, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page, BASE_URL);
const ok = await selectProject(page, project);
if (!ok) throw new Error('工程选择失败');
console.log(' ✓ 工程已选');
// 进 PCF/PCRF 列表(用 JS 点击 sidebar 链接,兼容折叠菜单)
await page.evaluate(() => {
const links = document.querySelectorAll('a[href*="/pcf/"]');
for (const l of links) {
if (l.textContent.trim().includes('PCF')) { l.click(); return; }
}
});
await page.waitForTimeout(3000);
console.log(' ✓ 进入PCF列表,URL:', page.url());
// 点添加按钮(弹窗)
await page.waitForSelector('button:has-text("添加")', { timeout: 10000 }).catch(() => {});
await page.locator('button:has-text("添加")').first().click();
await page.waitForTimeout(2000);
console.log(' ✓ 点添加(弹窗)');
// 切换到弹窗 iframe
await page.locator('iframe[name="layui-layer-iframe2"]').waitFor({ timeout: 5000 });
const frame = page.frame('layui-layer-iframe2');
if (!frame) throw new Error('未找到弹窗 iframe');
await frame.waitForLoadState('domcontentloaded');
console.log(' ✓ 切换到弹窗iframe');
// 填名称
await frame.locator('input[name="name"]').fill(name);
console.log(` ✓ name = name`);
// 类型下拉:点击"请选择"
await frame.getByRole('textbox', { name: '请选择' }).click();
await frame.getByRole('definition').filter({ hasText: '仿真设备' }).click();
await page.waitForTimeout(500);
console.log(' ✓ 类型 = 仿真设备');
// 数量
await frame.locator('input[name="count"]').fill('1');
console.log(' ✓ count = 1');
// HTTP2 SIP
await frame.locator('input[name="http2_sip"]').fill(http2_sip);
console.log(` ✓ http2_sip = http2_sip`);
// HTTP2 PORT
await frame.locator('input[name="http2_port"]').fill(http2_port);
console.log(` ✓ http2_port = http2_port`);
// MCC - label 为"三位数字"
await frame.getByRole('textbox', { name: '三位数字', exact: true }).fill(mcc);
console.log(` ✓ MCC = mcc`);
// MNC - label 为"二位或三位数字"
await frame.getByRole('textbox', { name: '二位或三位数字' }).fill(mnc);
console.log(` ✓ MNC = mnc`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(' ✓ 已提交');
const url = page.url();
if (url.includes('/pcf/index')) {
console.log(` ✅ 添加成功,URL: url`);
} else {
console.log(` ⚠️ 可能未保存,URL: url`);
}
await browser.close();
}
main().catch(e => { console.error('❌', e.message); process.exit(1); });
FILE:scripts/qos-add-skill.js
/**
* qos-add-skill.js - QoS模板添加工具
*
* 用法:
* node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000 [--headed]
* node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --5qi 8 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000 [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --qos-id QoS模板ID(必填)
* --5qi 5QI值(不指定则自动从已有5qi列表中选择一个不同的值)
* --maxbr-ul 上行最大比特率(不指定则用默认值)
* --maxbr-dl 下行最大比特率(不指定则用默认值)
* --gbr-ul 上行保证比特率(不指定则用默认值)
* --gbr-dl 下行保证比特率(不指定则用默认值)
* --priority 优先级(默认空)
* --headed 显示浏览器窗口
*
* 默认值(用户未指定时):
* maxbrUl=10000000, maxbrDl=20000000, gbrUl=5000000, gbrDl=5000000
* 5qi=自动选择(从已有5qi列表中挑一个不存在的值,优先8/9/6/5...)
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
qosId: null,
qi: null,
maxbrUl: null,
maxbrDl: null,
gbrUl: null,
gbrDl: null,
priority: '',
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--qos-id') opts.qosId = args[++i];
else if (args[i] === '--5qi') opts.qi = args[++i];
else if (args[i] === '--maxbr-ul') opts.maxbrUl = args[++i];
else if (args[i] === '--maxbr-dl') opts.maxbrDl = args[++i];
else if (args[i] === '--gbr-ul') opts.gbrUl = args[++i];
else if (args[i] === '--gbr-dl') opts.gbrDl = args[++i];
else if (args[i] === '--priority') opts.priority = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.qosId) {
console.error('❌ 缺少必要参数: --qos-id');
console.error(' 示例: node qos-add-skill.js --project XW_SUPF_5_1_2_4 --qos-id qos3 --maxbr-ul 10000000 --maxbr-dl 20000000 --gbr-ul 5000000 --gbr-dl 5000000');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
const icon = cells[1].querySelector('.iconfont');
if (icon) { icon.click(); return true; }
}
}
return false;
}, projectName);
if (!clicked) { console.log('❌ 未找到工程'); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function getUsed5qis(page) {
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const usedQis = await page.evaluate(() => {
const qis = new Set();
document.querySelectorAll('.layui-table tbody tr').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 4) {
const qi = parseInt(cells[3].textContent.trim());
if (!isNaN(qi)) qis.add(qi);
}
});
return [...qis];
});
return usedQis;
}
function autoSelect5qi(usedQis) {
const candidates = [8, 9, 6, 5, 4, 3, 2, 1];
for (const c of candidates) {
if (!usedQis.includes(c)) return c;
}
return 8;
}
const DEFAULT_BR = { maxbrUl: '10000000', maxbrDl: '20000000', gbrUl: '5000000', gbrDl: '5000000' };
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 自动确定5qi(用户未指定时)
if (opts.qi === null) {
console.log('\n📋 检测已有QoS模板的5qi...');
const usedQis = await getUsed5qis(page);
console.log(` 已使用5qi: usedQis.join(', ')`);
opts.qi = autoSelect5qi(usedQis);
console.log(` ✅ 自动选择 5qi = opts.qi(与已有不同)`);
} else {
console.log(`\n📋 用户指定 5qi = opts.qi`);
}
// 应用默认值
const params = {
qosId: opts.qosId,
qi: opts.qi,
maxbrUl: opts.maxbrUl || DEFAULT_BR.maxbrUl,
maxbrDl: opts.maxbrDl || DEFAULT_BR.maxbrDl,
gbrUl: opts.gbrUl || DEFAULT_BR.gbrUl,
gbrDl: opts.gbrDl || DEFAULT_BR.gbrDl,
};
console.log('\n📋 最终参数:');
console.log(` qosId = params.qosId`);
console.log(` 5qi = params.qi`);
console.log(` maxbrUl = params.maxbrUl`);
console.log(` maxbrDl = params.maxbrDl`);
console.log(` gbrUl = params.gbrUl`);
console.log(` gbrDl = params.gbrDl`);
// 去添加页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
await page.waitForTimeout(1000);
// 填写字段(使用 first() 确保能获取到元素)
await frame.locator('input[name="qosId"]').first().fill(params.qosId);
await frame.locator('input[name="5qi"]').first().fill(params.qi);
await frame.locator('input[name="maxbrUl"]').first().fill(params.maxbrUl);
await frame.locator('input[name="maxbrDl"]').first().fill(params.maxbrDl);
await frame.locator('input[name="gbrUl"]').first().fill(params.gbrUl);
await frame.locator('input[name="gbrDl"]').first().fill(params.gbrDl);
// 提交
await frame.locator('button:has-text("提交")').first().click();
await page.waitForTimeout(3000);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/qos/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const qosData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
for (let i = 0; i < cells.length; i++) {
if (cells[i].textContent.trim() === targetId) {
return {
id: cells[1].textContent.trim(),
qi: cells[3].textContent.trim(),
maxbrUl: cells[4].textContent.trim(),
maxbrDl: cells[5].textContent.trim(),
gbrUl: cells[6].textContent.trim(),
gbrDl: cells[7].textContent.trim(),
};
}
}
}
return null;
}, params.qosId);
if (qosData) {
console.log('\n📋 保存的QoS数据:');
console.log(` ID=qosData.id, 5qi=qosData.qi, maxbrUl=qosData.maxbrUl, maxbrDl=qosData.maxbrDl, gbrUl=qosData.gbrUl, gbrDl=qosData.gbrDl`);
const ok = qosData.qi === params.qi && qosData.maxbrUl === params.maxbrUl && qosData.maxbrDl === params.maxbrDl && qosData.gbrUl === params.gbrUl && qosData.gbrDl === params.gbrDl;
console.log(ok ? '\n✅ QoS模板创建成功!' : '\n⚠️ 部分数据可能未正确保存');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy-ue-add-skill.js
/**
* smpolicy-ue-add-skill.js - UE Smpolicy 添加工具
*
* 用法:
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet --imsi 460001234567890
* node smpolicy-ue-add-skill.js --project XW_SUPF_5_1_2_4 --name ue_test --dnn internet --sst 1 --sd 111111 --pcc-rules pcc2
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --name UE策略名称(必填)
* --dnn DNN(必填)
* --imsi IMSI起始值(可选,不填则自动生成)
* --imsi-num IMSI数量(默认 1)
* --sst sNssai SST(默认 1)
* --sd sNssai SD(默认 111111)
* --sess-rules 会话规则名称(xm-select,多个逗号分隔)
* --pcc-rules PCC规则名称(xm-select,多个逗号分隔)
* --pra-rules PRA规则名称(xm-select,可选)
* --ref-qos-timer reflectiveQoSTimer 值(可选)
* --headed 显示浏览器窗口
*
* 添加页:/sim_5gc/smpolicy/ue/edit(layui-layer-iframe2)
* xm-select: sessRules=idx0, pccRules=idx1, praRules=idx2
*
* xm-select 交互:
* 1. frame.evaluate(() => inputs[idx].parentElement.click()) 打开下拉
* 2. frame.locator('.xm-option', {hasText}).click() 选择选项
* 3. page.keyboard.press('Escape') 关闭下拉
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
name: null,
dnn: null,
imsi: null,
imsiNum: '1',
sst: '1',
sd: '111111',
sessRules: null,
pccRules: null,
praRules: null,
refQosTimer: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--name') opts.name = args[++i];
else if (args[i] === '--dnn') opts.dnn = args[++i];
else if (args[i] === '--imsi') opts.imsi = args[++i];
else if (args[i] === '--imsi-num') opts.imsiNum = args[++i];
else if (args[i] === '--sst') opts.sst = args[++i];
else if (args[i] === '--sd') opts.sd = args[++i];
else if (args[i] === '--sess-rules') opts.sessRules = args[++i];
else if (args[i] === '--pcc-rules') opts.pccRules = args[++i];
else if (args[i] === '--pra-rules') opts.praRules = args[++i];
else if (args[i] === '--ref-qos-timer') opts.refQosTimer = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.name) { console.error('❌ 缺少 --name 参数'); process.exit(1); }
if (!opts.dnn) { console.error('❌ 缺少 --dnn 参数'); process.exit(1); }
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, name) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(name);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const found = await page.evaluate((n) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === n) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, name);
if (!found) { console.error(`❌ 未找到工程: name`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "name" 已选`);
}
/**
* 选择 xm-select 中的一个选项(支持多选,同一选项点击可切换选中状态)
*/
async function xmSelectChooseOne(frame, page, index, value) {
if (!value) return;
// 打开下拉
await frame.evaluate((idx) => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[idx]) inputs[idx].parentElement.click();
}, index);
await page.waitForTimeout(1000);
// 点击目标选项
const clicked = await frame.evaluate((text) => {
const opts = document.querySelectorAll('.xm-option');
for (const opt of opts) {
if (opt.textContent.trim() === text) {
opt.click();
return true;
}
}
return false;
}, value);
if (clicked) {
console.log(` ✅ xm-select[index] = value`);
} else {
console.log(` ⚠️ xm-select[index] 未找到选项: value`);
}
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
}
/**
* 选择 xm-select 中的多个选项(逗号分隔)
*/
async function xmSelectChooseMultiple(frame, page, index, values) {
if (!values) return;
const items = values.split(',').map(s => s.trim()).filter(Boolean);
for (const item of items) {
await xmSelectChooseOne(frame, page, index, item);
}
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 导航到 UE smpolicy 列表页
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达 UE Smpolicy 列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(3000);
// 获取编辑帧
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
await frame.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000);
console.log(`✅ 进入弹窗iframe: frame.url()`);
// ① 填写文本字段
// 自动生成 IMSI(如果未提供)
const autoImsi = opts.imsi || `4600Date.now().toString().slice(-10)`;
const textFields = [
{ name: 'name', value: opts.name },
{ name: 'dnn', value: opts.dnn },
{ name: 'imsi', value: autoImsi },
{ name: 'imsi_num', value: opts.imsiNum },
{ name: 'sNssai[sst]', value: opts.sst },
{ name: 'sNssai[sd]', value: opts.sd },
];
if (opts.refQosTimer) {
textFields.push({ name: 'smPolicyDecision[reflectiveQoSTimer]', value: opts.refQosTimer });
}
for (const f of textFields) {
const loc = frame.locator(`[name="f.name"]`).first();
if (await loc.count() > 0) {
await loc.fill(String(f.value));
console.log(` ✅ f.name = "f.value"`);
} else {
console.log(` ⚠️ 字段 f.name 不存在`);
}
}
// ② xm-select 选择(sessRules=idx0, pccRules=idx1, praRules=idx2)
// sessRules 通常无数据(暂无数据),有则选
const sessDisplay = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[0]?.parentElement?.textContent || '';
});
if (!sessDisplay.includes('暂无数据')) {
await xmSelectChooseMultiple(frame, page, 0, opts.sessRules);
} else if (opts.sessRules) {
console.log(` ℹ️ sessRules 无可用数据,跳过`);
}
// pccRules
await xmSelectChooseMultiple(frame, page, 1, opts.pccRules);
// praRules 通常无数据
const praDisplay = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
return inputs[2]?.parentElement?.textContent || '';
});
if (!praDisplay.includes('暂无数据')) {
await xmSelectChooseMultiple(frame, page, 2, opts.praRules);
} else if (opts.praRules) {
console.log(` ℹ️ praRules 无可用数据,跳过`);
}
// ③ 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ 已提交`);
// ④ 验证:回到列表页检查
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/ue/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const added = await page.evaluate((targetName) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 8 && cells[2].textContent.trim() === targetName) {
return {
name: cells[2].textContent.trim(),
dnn: cells[3].textContent.trim(),
sst: cells[4].textContent.trim(),
sd: cells[5].textContent.trim(),
sessRules: cells[6].textContent.trim(),
pccRules: cells[7].textContent.trim(),
};
}
}
return null;
}, opts.name);
if (added) {
console.log('\n📋 验证结果:');
console.log(` name = added.name '❌'`);
console.log(` dnn = added.dnn '❌'`);
console.log(` sst = added.sst`);
console.log(` sd = added.sd`);
console.log(` sessRules = added.sessRules`);
console.log(` pccRules = added.pccRules`);
if (opts.pccRules) {
const expectedPccs = opts.pccRules.split(',').map(s => s.trim());
const match = expectedPccs.every(p => added.pccRules.includes(p));
console.log(` pccRules 匹配: '⚠️'`);
}
} else {
console.log('\n❌ 未在列表中找到创建的 UE Smpolicy');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/smpolicy_add_pcc.js
/**
* smpolicy_add_pcc.js - 将 PCC 规则添加到 sm_policy_default 的 pccRules
*
* 用法:
* node smpolicy_add_pcc.js --project XW_SUPF_5_1_2_4 --pcc-id pcc_new
*
* 参数:
* --project 工程名(默认 XW_SUPF_5_1_2_4)
* --pcc-id PCC规则ID(必填,需已存在)
* --headed 显示浏览器窗口
*
* 完整链路:
* smpolicy/default/index → 编辑 sm_policy_default 弹窗(iframe)
* → pccRules xm-select(第1个)中添加 --pcc-id
* → 提交
*
* xm-select 交互(Playwright locator):
* 1. JS: inputs[idx].parentElement.click() 打开下拉
* 2. frame.locator('.xm-option.show-icon', {hasText}).click() 选择选项
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_SUPF_5_1_2_4',
pccId: null,
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--pcc-id') opts.pccId = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.pccId) {
console.error('❌ 缺少 --pcc-id 参数');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000 });
await page.waitForTimeout(1500);
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
let result = false;
document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row').forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont')?.click();
result = true;
}
});
return result;
}, projectName);
if (!clicked) { console.error(`❌ 未找到工程: projectName`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 导航到 smpolicy/default/index
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log('✅ 到达 smpolicy/default/index');
// 点击"编辑"按钮(第1行 sm_policy_default)
await page.evaluate(() => {
const rows = document.querySelectorAll('.layui-table tbody tr');
if (rows.length > 0) {
const editBtn = rows[0].querySelector('a');
if (editBtn) editBtn.click();
}
});
await page.waitForTimeout(3000);
// 进入弹窗 iframe
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
console.log('✅ 进入弹窗iframe');
// 检查当前 pccRules 的值(pccRules = xm-select[1])
const before = await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs.length >= 2) {
return {
pccRulesValue: inputs[1].value,
pccRulesDisplay: inputs[1].parentElement.textContent.substring(0, 80),
};
}
return null;
});
console.log('\n📋 编辑前状态:', JSON.stringify(before));
// 打开 pccRules 下拉(第1个xm-select)
console.log(`\n▶ 添加 opts.pccId 到 pccRules...`);
await frame.evaluate(() => {
const inputs = document.querySelectorAll('input.xm-select-default');
if (inputs[1]) inputs[1].parentElement.click();
});
await page.waitForTimeout(1000);
// 用 Playwright locator 点击选项
const optLocator = frame.locator('.xm-option.show-icon', { hasText: opts.pccId });
const visible = await optLocator.isVisible({ timeout: 3000 }).catch(() => false);
if (visible) {
await optLocator.click();
console.log(` ✅ 选择 opts.pccId`);
} else {
console.log(` ❌ 选项 opts.pccId 不可见`);
const availOpts = await frame.evaluate(() =>
Array.from(document.querySelectorAll('.xm-option.show-icon')).map(o => o.textContent.trim())
);
console.log(` 可用选项: availOpts.join(', ')`);
}
await page.waitForTimeout(500);
// 关闭 xm-select 下拉(按 Escape 避免遮罩层)
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log('✅ 已提交');
// 验证
await page.goto(`globalBaseUrl/sim_5gc/smpolicy/default/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const updated = await page.evaluate((targetPccId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 6 && cells[2].textContent.trim() === 'sm_policy_default') {
return { pccRules: cells[4].textContent.trim() };
}
}
return null;
}, opts.pccId);
if (updated) {
console.log('\n📋 更新后 pccRules:', updated.pccRules);
console.log(updated.pccRules.includes(opts.pccId) ? `\n🎉 成功将 opts.pccId 添加到 pccRules!` : `\n⚠️ 未检测到 opts.pccId`);
}
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });
FILE:scripts/tc-add-skill.js
/**
* tc-add-skill.js - Traffic Control 流量控制模板添加工具
*
* 用法:
* node tc-add-skill.js --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED-UPLINK [--headed]
*
* 参数:
* --project 工程名(默认 XW_S5GC_1)
* --tc-id TC模板ID(必填,字母/数字/下划线)
* --flow-status flowStatus(默认 ENABLED-UPLINK)
* 可选值:ENABLED-UPLINK, ENABLED-DOWNLINK, ENABLED, DISABLED, REMOVED
* --headed 显示浏览器窗口
*
* 完整链路:
* 点击"添加" → 弹窗 iframe(layui-layer-iframe2)→ 填写 tcId + flowStatus(SELECT)
* → 提交 → 返回列表页
*
* 注意事项:
* - flowStatus 是 SELECT 下拉框,用 JS 方式设置值(layui 隐藏原生select)
* - tcId 是必填字段
*/
const { chromium } = require('playwright');
const globalBaseUrl = 'https://192.168.3.89';
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
project: 'XW_S5GC_1',
tcId: null,
flowStatus: 'ENABLED-UPLINK',
headed: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project' || args[i] === '-p') opts.project = args[++i];
else if (args[i] === '--tc-id') opts.tcId = args[++i];
else if (args[i] === '--flow-status') opts.flowStatus = args[++i];
else if (args[i] === '--headed') opts.headed = true;
}
if (!opts.tcId) {
console.error('❌ 缺少必要参数: --tc-id');
console.error(' 示例: node tc-add-skill.js --project XW_SUPF_5_1_2_4 --tc-id tc_new --flow-status ENABLED-UPLINK');
process.exit(1);
}
return opts;
}
async function login(page) {
await page.goto(`globalBaseUrl/login`, { ignoreHTTPSErrors: true, timeout: 15000, waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
try { await page.locator('input[name="email"]').first().waitFor({ state: 'visible', timeout: 5000 }); } catch(e) {}
await page.getByRole('textbox', { name: 'E-Mail地址' }).fill('[email protected]');
await page.getByRole('textbox', { name: '密码' }).fill('dotouch');
await page.getByRole('button', { name: '登录' }).click();
await page.waitForTimeout(2500);
console.log('✅ 登录成功');
}
async function selectProject(page, projectName) {
await page.goto(`globalBaseUrl/sim_5gc/project/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(2000);
await page.locator('input[name="project_search_name"]').fill(projectName);
await page.keyboard.press('Enter');
await page.waitForTimeout(3000);
const clicked = await page.evaluate((name) => {
const rows = document.querySelectorAll('.jsgrid-row, .jsgrid-alt-row');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 3 && cells[2].textContent.trim() === name) {
cells[1].querySelector('.iconfont').click();
return true;
}
}
return false;
}, projectName);
if (!clicked) { console.error(`❌ 未找到工程: projectName`); process.exit(1); }
await page.waitForTimeout(3000);
console.log(`✅ 工程 "projectName" 已选`);
}
async function main() {
const opts = parseArgs();
const browser = await chromium.launch({ headless: !opts.headed, args: ['--no-sandbox', '--ignore-certificate-errors', '--disable-dev-shm-usage', '--no-proxy-server', '--proxy-server=direct://', '--proxy-bypass-list=*'] });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1920, height: 1080 } });
const page = await ctx.newPage();
await login(page);
await selectProject(page, opts.project);
// 去 TC 列表页
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
console.log(`✅ 到达TC列表页`);
// 点击添加按钮
await page.locator('button:has-text("添加")').click();
await page.waitForTimeout(5000);
// 获取弹窗 iframe(layui-layer-iframe2)
const frame = page.frame('layui-layer-iframe2');
if (!frame) { console.error('❌ 未找到弹窗iframe'); process.exit(1); }
console.log(`✅ 进入弹窗iframe`);
// 填写 tcId
await frame.locator('input[name="tcId"]').fill(opts.tcId);
console.log(` tcId="opts.tcId"`);
// 设置 flowStatus(用 JS 方式,因为 layui 隐藏了原生 select)
await frame.evaluate((status) => {
const sel = document.querySelector('select[name="flowStatus"]');
if (sel) { sel.value = status; sel.dispatchEvent(new Event('change', { bubbles: true })); }
}, opts.flowStatus);
console.log(` flowStatus="opts.flowStatus"`);
// 提交
await frame.locator('button:has-text("提交")').click();
await page.waitForTimeout(3000);
console.log(`✅ TC模板 opts.tcId 已提交`);
// 验证
await page.goto(`globalBaseUrl/sim_5gc/predfPolicy/trafficCtl/index`, { waitUntil: 'networkidle', ignoreHTTPSErrors: true });
await page.waitForTimeout(3000);
const tcData = await page.evaluate((targetId) => {
const rows = document.querySelectorAll('.layui-table tbody tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 4 && cells[2].textContent.trim() === targetId) {
return { tcId: cells[2].textContent.trim(), flowStatus: cells[3].textContent.trim() };
}
}
return null;
}, opts.tcId);
if (tcData) {
console.log('\n📋 验证结果:');
console.log(` tcId = tcData.tcId`);
console.log(` flowStatus = tcData.flowStatus '❌'`);
} else {
console.log('\n❌ TC模板未找到');
}
console.log('\n✅ 完成');
await browser.close();
}
main().catch(e => { console.error(e); process.exit(1); });structured academic paper analysis from local paper files or paper urls, adapted from a dify scheme a workflow. use when the user asks to analyze pdf/docx/te...
---
name: paper-analysis-evidence
description: structured academic paper analysis from local paper files or paper urls, adapted from a dify scheme a workflow. use when the user asks to analyze pdf/docx/text/html academic papers, extract title/task/background/problem/method/datasets/baselines/metrics/results/ablations/limitations/contributions, cite evidence spans, verify consistency against the original paper, or export paper analysis reports. supports chinese or english outputs and saves downloaded inputs, intermediate files, generated json, markdown, html, and docx reports under the ubuntu desktop.
---
# Paper Analysis Evidence
## Purpose
Run the Scheme A evidence-enhanced paper analysis workflow: prepare paper inputs, split the paper into key sections, generate structured extraction JSON, verify the extraction against the original text, and render final reports.
This skill is based on the uploaded Dify workflow `论文分析系统_方案A_结构化证据增强版`.
## Runtime file policy
Always save runtime downloads and generated outputs under the Ubuntu desktop unless the user explicitly requests another location:
```bash
~/Desktop/paper_analysis_results/<YYYYMMDD_HHMMSS>/
```
Do not modify the original local paper file. Copy it into the work directory before extraction. Download URL inputs into the same batch work directory.
## Inputs
Accept:
- `language`: `中文` or `英文`; default to `中文` when unspecified.
- `paper_files`: one or more local paper files, preferably PDF, DOCX, TXT, MD, or HTML.
- `paper_urls`: one or more PDF/direct paper URLs, comma-separated or repeated.
If both local files and URLs are empty, stop with this message:
```text
上传的文件和论文URL不能同时为空。
```
## Workflow
### 1. Prepare inputs and sections
Run:
```bash
python scripts/prepare_papers.py --language 中文 --files /path/to/paper.pdf --urls "https://example.com/paper.pdf"
```
Use only the relevant arguments. For URL-only runs, omit `--files`; for local-only runs, omit `--urls`.
The script creates `manifest.json` and one work directory per paper. It performs:
1. local file copy or URL download,
2. raw text extraction,
3. text cleaning,
4. section splitting into abstract, intro, method, experiment, conclusion, and `paper_body`,
5. prompt file generation.
### 2. Generate structured extraction JSON
For each paper in `manifest.json`, read:
```text
prompts/01_structured_extraction_prompt.md
```
Send that prompt to the model. Save the model response exactly as JSON-only content to:
```text
generated/structured_result.json
```
Required JSON fields:
```json
{
"title": "",
"task": "",
"background": "",
"problem_statement": "",
"method_name": "",
"method_core": "",
"datasets": [],
"baselines": [],
"metrics": [],
"main_results": [
{"dataset": "", "metric": "", "value": "", "baseline": "", "improvement": ""}
],
"ablations": [],
"limitations": [],
"claims": [],
"contributions": [],
"evidence_spans": [
{"field": "", "claim": "", "evidence": ""}
]
}
```
Extraction rules:
- Only use information present in, or directly inferable from, the paper.
- Prefer corresponding sections, but fall back to the full `paper_body` when a section is empty or insufficient.
- Do not leave `datasets`, `baselines`, or `metrics` empty just because the experiment section is weak; first check `paper_body`, result text, implementation details, and table-neighboring text.
- Use empty strings or arrays only when the full paper text truly lacks the information.
- Provide at least 6 evidence spans. Each evidence span must be a direct quote or a very close paraphrase from the source text.
- Prioritize numeric results from experiment, results, analysis, implementation details, or table-neighboring text.
- Keep JSON keys in English. Natural-language values must use the selected output language.
### 3. Run consistency verification
Open:
```text
prompts/02_verification_prompt_template.md
```
Replace `{{structured_json}}` with the actual content of `generated/structured_result.json`. Send the complete verification prompt to the model and save JSON-only output to:
```text
generated/verification_result.json
```
Required verification JSON:
```json
{
"overall_score": 0,
"hallucination_risk": "low/medium/high",
"issues": [
{"field": "", "problem": "", "severity": "low/medium/high"}
],
"verified_claims": [
{"claim": "", "status": "supported/weak/unsupported", "evidence": ""}
],
"final_verdict": ""
}
```
Verification rules:
- Score 5: nearly no hallucination, strong evidence.
- Score 4: minor imprecision.
- Score 3: several claims lack evidence.
- Score 2: clear inconsistency exists.
- Score 1: substantial hallucination or misreading.
- Focus on omitted or incorrect datasets, baselines, metrics, and main results.
- If the structured extraction uses an empty array/string for information that exists in the original paper, explicitly list that in `issues`.
- Provide at least 4 verified claims.
### 4. Render reports
After `structured_result.json` and `verification_result.json` are saved for every paper, run:
```bash
python scripts/render_report.py --manifest ~/Desktop/paper_analysis_results/<YYYYMMDD_HHMMSS>/manifest.json
```
Outputs per paper:
```text
report/final_report.md
report/final_report.html
report/final_report.docx
```
The `.md` file preserves editable Markdown source. The `.html` file is the rendered visual version. The `.docx` file is the Word-compatible report.
## Report structure
Chinese report sections:
1. 论文题目
2. 任务与问题
3. 方法概述
4. 实验要素:数据集、基线方法、评价指标
5. 主要结果
6. 贡献提炼
7. 消融与局限性
8. 证据片段
9. 一致性校验:总评分、幻觉风险、最终结论、已核验结论、发现的问题
English report sections mirror the same structure as `Paper Analysis`.
## References
- Use `references/prompt_templates.md` when prompt details are needed.
- Use `references/workflow_mapping.md` when checking how the Dify nodes map to this skill.
- `references/dify_scheme_a_source.yml` preserves the uploaded Dify DSL source for auditability.
FILE:scripts/prepare_papers.py
#!/usr/bin/env python3
"""Prepare paper inputs for the evidence-enhanced paper analysis workflow.
This script handles the deterministic parts of the Dify Scheme A workflow:
- validate that at least one local file or URL is provided
- copy local files and download URL inputs into a desktop work directory
- extract text from common document formats
- clean text
- split text into paper sections
- write prompt-ready files for LLM structured extraction and verification
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import time
import urllib.parse
import urllib.request
import zipfile
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Iterable
MIN_TEXT_LENGTH = 800
def desktop_root() -> Path:
return Path.home() / "Desktop" / "paper_analysis_results"
def timestamp() -> str:
return time.strftime("%Y%m%d_%H%M%S")
def sanitize_filename(name: str, fallback: str = "paper") -> str:
name = urllib.parse.unquote(name or "")
name = name.strip().replace("\\", "/").split("/")[-1]
name = re.sub(r"[^A-Za-z0-9._\-\u4e00-\u9fff]+", "_", name).strip("._")
return name or fallback
def split_csv(values: Iterable[str] | None) -> list[str]:
result: list[str] = []
for value in values or []:
for item in (value or "").split(","):
item = item.strip()
if item:
result.append(item)
return result
def safe_copy_file(src: Path, dest_dir: Path) -> Path:
if not src.exists() or not src.is_file():
raise FileNotFoundError(f"local paper file not found: {src}")
dest_dir.mkdir(parents=True, exist_ok=True)
dest = dest_dir / sanitize_filename(src.name)
if dest.exists():
stem, suffix = dest.stem, dest.suffix
i = 2
while True:
candidate = dest_dir / f"{stem}_{i}{suffix}"
if not candidate.exists():
dest = candidate
break
i += 1
shutil.copy2(src, dest)
return dest
def guess_download_name(url: str, content_type: str | None = None) -> str:
path_name = sanitize_filename(urllib.parse.urlparse(url).path, "paper")
if "." in path_name:
return path_name
if content_type:
ct = content_type.lower()
if "pdf" in ct:
return path_name + ".pdf"
if "word" in ct or "docx" in ct:
return path_name + ".docx"
if "html" in ct:
return path_name + ".html"
if "text" in ct:
return path_name + ".txt"
return path_name + ".pdf"
def download_url(url: str, dest_dir: Path, timeout: int = 60, max_retries: int = 2) -> Path:
dest_dir.mkdir(parents=True, exist_ok=True)
last_error: Exception | None = None
for attempt in range(max_retries + 1):
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 paper-analysis-evidence"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
content_type = resp.headers.get("Content-Type")
name = guess_download_name(url, content_type)
dest = dest_dir / name
if dest.exists():
stem, suffix = dest.stem, dest.suffix
digest = hashlib.sha1(url.encode("utf-8")).hexdigest()[:8]
dest = dest_dir / f"{stem}_{digest}{suffix}"
with open(dest, "wb") as f:
shutil.copyfileobj(resp, f)
return dest
except Exception as exc: # pragma: no cover - depends on network
last_error = exc
if attempt < max_retries:
time.sleep(1.5 * (attempt + 1))
raise RuntimeError(f"failed to download {url}: {last_error}")
def extract_pdf_text(path: Path) -> str:
errors: list[str] = []
for module_name in ("pypdf", "PyPDF2"):
try:
module = __import__(module_name)
reader = module.PdfReader(str(path))
pages = []
for page in reader.pages:
pages.append(page.extract_text() or "")
text = "\n\n".join(pages).strip()
if text:
return text
except Exception as exc:
errors.append(f"{module_name}: {exc}")
try:
completed = subprocess.run(
["pdftotext", str(path), "-"],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=120,
)
text = completed.stdout.strip()
if text:
return text
except Exception as exc:
errors.append(f"pdftotext: {exc}")
raise RuntimeError("could not extract PDF text. install pypdf or poppler-utils. " + "; ".join(errors))
def extract_docx_text(path: Path) -> str:
with zipfile.ZipFile(path) as zf:
xml = zf.read("word/document.xml").decode("utf-8", errors="ignore")
xml = re.sub(r"</w:p>", "\n", xml)
xml = re.sub(r"<[^>]+>", "", xml)
entities = {"&": "&", "<": "<", ">": ">", """: '"', "'": "'"}
for k, v in entities.items():
xml = xml.replace(k, v)
return xml.strip()
def extract_html_text(path: Path) -> str:
text = path.read_text(encoding="utf-8", errors="ignore")
text = re.sub(r"(?is)<script[^>]*>.*?</script>", " ", text)
text = re.sub(r"(?is)<style[^>]*>.*?</style>", " ", text)
text = re.sub(r"(?i)<br\s*/?>", "\n", text)
text = re.sub(r"(?i)</p>", "\n", text)
text = re.sub(r"<[^>]+>", " ", text)
text = text.replace(" ", " ").replace("&", "&").replace("<", "<").replace(">", ">")
return text.strip()
def extract_text(path: Path) -> str:
suffix = path.suffix.lower()
if suffix == ".pdf":
return extract_pdf_text(path)
if suffix == ".docx":
return extract_docx_text(path)
if suffix in {".txt", ".md", ".markdown"}:
return path.read_text(encoding="utf-8", errors="ignore")
if suffix in {".html", ".htm"}:
return extract_html_text(path)
try:
return path.read_text(encoding="utf-8", errors="ignore")
except Exception as exc:
raise RuntimeError(f"unsupported document format: {path.name}. Use PDF, DOCX, TXT, MD, or HTML. {exc}")
def clean_for_analysis(raw_text: str, min_length: int = MIN_TEXT_LENGTH) -> tuple[str, int, str]:
if not raw_text or not isinstance(raw_text, str):
return "", 0, "input text is empty or invalid"
text = raw_text.replace("\r\n", "\n").replace("\r", "\n")
text = re.sub(r"\n{3,}", "\n\n", text)
text = re.sub(r"[ \t]+", " ", text)
text = re.sub(r"\nReferences[\s\S]*$", "", text, flags=re.I)
text = re.sub(r"\nBibliography[\s\S]*$", "", text, flags=re.I)
text = text.strip()
word_count = len(text)
if word_count < min_length:
return text, word_count, f"text is short ({word_count} chars); extraction may have failed"
return text, word_count, ""
def normalize_newlines(text: str) -> str:
text = (text or "").replace("\r\n", "\n").replace("\r", "\n")
text = re.sub(r"[ \t]+", " ", text)
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()
def trim_references(text: str) -> str:
if not text:
return ""
patterns = [r"\n\s*(references|bibliography)\s*$", r"\n\s*参考文献\s*$"]
cut_positions = []
for p in patterns:
m = re.search(p, text, flags=re.I | re.M)
if m:
cut_positions.append(m.start())
if cut_positions:
text = text[:min(cut_positions)]
return text.strip()
def heading_patterns() -> dict[str, list[str]]:
return {
"abstract": [r"abstract", r"摘要"],
"intro": [r"introduction", r"intro", r"引言", r"绪论"],
"related": [r"related work", r"background", r"preliminar(?:y|ies)", r"相关工作", r"背景", r"预备知识"],
"method": [
r"method", r"methods", r"methodology", r"approach", r"approaches", r"framework", r"model", r"architecture",
r"proposed method", r"proposed approach", r"our method", r"our approach", r"方法", r"模型", r"框架", r"架构",
],
"experiment": [
r"experiment", r"experiments", r"experimental setup", r"evaluation", r"evaluations", r"results", r"analysis",
r"implementation details", r"empirical study", r"实验", r"实验设置", r"实验结果", r"评估", r"结果", r"分析", r"实现细节",
],
"conclusion": [r"conclusion", r"conclusions", r"discussion", r"limitations", r"future work", r"结论", r"讨论", r"局限性", r"未来工作"],
"tail": [r"acknowledg(?:e)?ments?", r"appendix", r"references", r"bibliography", r"致谢", r"附录", r"参考文献"],
}
def line_is_heading(line: str) -> bool:
s = line.strip()
return bool(s) and len(s) <= 120 and s.count(". ") < 2
def detect_heading_type(line: str, patterns: dict[str, list[str]]) -> str | None:
s = line.strip()
if not line_is_heading(s):
return None
normalized = re.sub(
r"^\s*(section\s+)?((\d+(\.\d+)*)|[ivxlcdm]+|第\s*\d+\s*[章节部分])[\.\:\-\s]*",
"",
s,
flags=re.I,
).strip()
for sec_type, pats in patterns.items():
for pat in pats:
if re.fullmatch(rf"{pat}", normalized, flags=re.I):
return sec_type
for sec_type, pats in patterns.items():
for pat in pats:
if re.search(rf"\b{pat}\b", normalized, flags=re.I):
return sec_type
return None
def collect_headings(text: str) -> list[dict[str, object]]:
patterns = heading_patterns()
headings: list[dict[str, object]] = []
offset = 0
for line in text.split("\n"):
sec_type = detect_heading_type(line, patterns)
if sec_type:
headings.append({"type": sec_type, "title": line.strip(), "start": offset})
offset += len(line) + 1
headings.sort(key=lambda x: int(x["start"]))
return headings
def get_section_by_heading(text: str, headings: list[dict[str, object]], target_type: str) -> str:
candidates = [h for h in headings if h["type"] == target_type]
if not candidates:
return ""
start = int(candidates[0]["start"])
later = [int(h["start"]) for h in headings if int(h["start"]) > start]
end = min(later) if later else len(text)
return text[start:end].strip()
def fallback_window(text: str, keywords: list[str], max_len: int = 10000, search_start: int = 0) -> str:
window_text = text[search_start:]
best: int | None = None
for kw in keywords:
m = re.search(kw, window_text, flags=re.I)
if m:
pos = search_start + m.start()
if best is None or pos < best:
best = pos
if best is None:
return ""
start = max(0, best - 150)
end = min(len(text), start + max_len)
return text[start:end].strip()
def normalize_section(section: str, max_len: int) -> str:
if not section:
return ""
section = section.strip()
section = re.sub(r"\n{3,}", "\n\n", section)
return section[:max_len]
def split_sections(cleaned_text: str) -> dict[str, str]:
text = trim_references(normalize_newlines(cleaned_text))
if not text:
return {
"abstract_section": "", "intro_section": "", "method_section": "",
"experiment_section": "", "conclusion_section": "", "paper_body": "",
}
headings = collect_headings(text)
abstract = get_section_by_heading(text, headings, "abstract")
if not abstract:
m = re.search(r"(^|\n)\s*(abstract|摘要)\s*\n", text, flags=re.I)
if m:
start = m.start()
next_heads = [int(h["start"]) for h in headings if int(h["start"]) > start]
end = min(next_heads) if next_heads else min(len(text), start + 6000)
abstract = text[start:end].strip()
else:
intro_heads = [int(h["start"]) for h in headings if h["type"] == "intro"]
if intro_heads and intro_heads[0] > 200:
abstract = text[:intro_heads[0]].strip()[:6000]
intro = get_section_by_heading(text, headings, "intro")
if not intro:
m = re.search(r"(introduction|引言|绪论)", text[:max(3000, len(text)//5)], flags=re.I)
if m:
start = max(0, m.start() - 100)
next_heads = [int(h["start"]) for h in headings if int(h["start"]) > start]
end = min(next_heads) if next_heads else min(len(text), start + 8000)
intro = text[start:end].strip()
method = get_section_by_heading(text, headings, "method")
if not method:
intro_heads = [int(h["start"]) for h in headings if h["type"] == "intro"]
method = fallback_window(text, [r"\bmethod\b", r"\bmethods\b", r"\bmethodology\b", r"\bapproach\b", r"\bframework\b", r"\barchitecture\b", r"\bmodel\b", r"方法", r"框架", r"架构", r"模型"], 12000, intro_heads[0] if intro_heads else 0)
experiment = get_section_by_heading(text, headings, "experiment")
if not experiment:
method_heads = [int(h["start"]) for h in headings if h["type"] == "method"]
experiment = fallback_window(text, [r"\bexperiments?\b", r"\bevaluation\b", r"\bresults\b", r"\banalysis\b", r"\bimplementation details\b", r"实验", r"评估", r"结果", r"分析", r"实现细节"], 12000, method_heads[0] if method_heads else 0)
conclusion = get_section_by_heading(text, headings, "conclusion")
if not conclusion:
conclusion = fallback_window(text, [r"\bconclusion\b", r"\bconclusions\b", r"\bdiscussion\b", r"\blimitations\b", r"\bfuture work\b", r"结论", r"讨论", r"局限", r"未来工作"], 6000, len(text)//2)
return {
"abstract_section": normalize_section(abstract, 6000),
"intro_section": normalize_section(intro, 8000),
"method_section": normalize_section(method, 12000),
"experiment_section": normalize_section(experiment, 12000),
"conclusion_section": normalize_section(conclusion, 6000),
"paper_body": text[:60000],
}
STRUCTURED_PROMPT_TEMPLATE = """你是一位严谨的论文信息抽取器。请严格基于给定论文内容,输出一个 JSON 对象,不要输出 Markdown、不要解释、不要加 ```json。
输出字段必须包含:
{{
"title": "",
"task": "",
"background": "",
"problem_statement": "",
"method_name": "",
"method_core": "",
"datasets": [],
"baselines": [],
"metrics": [],
"main_results": [
{{"dataset": "", "metric": "", "value": "", "baseline": "", "improvement": ""}}
],
"ablations": [],
"limitations": [],
"claims": [],
"contributions": [],
"evidence_spans": [
{{"field": "", "claim": "", "evidence": ""}}
]
}}
规则:
1. 只能写原文出现或可直接推出的信息,不要脑补。
2. 优先使用对应章节抽取信息;如果某章节为空或信息不足,必须从全文补充。
3. datasets、baselines、metrics 不允许因为章节为空而直接写空数组,必须先检查全文 paper_body、实验结果、实现细节、表格附近文本是否存在相关信息。
4. 只有在全文中也确实找不到时,才能写空字符串或空数组。
5. evidence_spans 至少给 6 条,每条都必须是原文中的直接证据片段或非常贴近原文的概括。
6. 数值结果优先来自实验部分、结果部分、分析部分或表格附近文本。
7. JSON 键名保持英文;JSON 中的自然语言内容使用 {language}。
论文摘要段:
{abstract_section}
引言段:
{intro_section}
方法段:
{method_section}
实验段:
{experiment_section}
结论段:
{conclusion_section}
全文:
{paper_body}
"""
VERIFICATION_PROMPT_TEMPLATE = """你是一位论文事实核验器。请对下面的结构化抽取结果做一致性校验,并输出 JSON 对象,不要输出 Markdown、不要解释。
输出格式:
{{
"overall_score": 0,
"hallucination_risk": "low/medium/high",
"issues": [
{{"field": "", "problem": "", "severity": "low/medium/high"}}
],
"verified_claims": [
{{"claim": "", "status": "supported/weak/unsupported", "evidence": ""}}
],
"final_verdict": ""
}}
评分规则:
- 5:几乎无幻觉,证据充分
- 4:少量不严谨
- 3:有若干缺证据表述
- 2:存在明显不一致
- 1:大量幻觉或错读
要求:
1. JSON 键名保持英文。
2. JSON 中的自然语言内容使用 {language}。
3. 重点检查 datasets、baselines、metrics、main_results 是否遗漏或误抽。
4. 若抽取结果将原文存在的信息写成空数组或空字符串,请在 issues 中明确指出。
5. verified_claims 至少给 4 条。
原文:
{paper_body}
待校验JSON:
{{structured_json}}
"""
@dataclass
class PaperRecord:
id: str
source_type: str
source: str
input_file: str
work_dir: str
raw_text_file: str
cleaned_text_file: str
sections_file: str
structured_prompt_file: str
verification_prompt_file: str
structured_json_file: str
verification_json_file: str
final_markdown_file: str
final_html_file: str
final_docx_file: str
language: str
word_count: int
warning: str = ""
def write_text(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
def prepare_one(input_path: Path, source_type: str, source: str, batch_dir: Path, language: str, index: int) -> PaperRecord:
paper_id = f"paper_{index:02d}_{sanitize_filename(input_path.stem, 'paper')}"
paper_dir = batch_dir / paper_id
for sub in ["input", "text", "sections", "prompts", "generated", "report"]:
(paper_dir / sub).mkdir(parents=True, exist_ok=True)
kept = safe_copy_file(input_path, paper_dir / "input") if input_path.parent != paper_dir / "input" else input_path
raw = extract_text(kept)
cleaned, word_count, warning = clean_for_analysis(raw)
sections = split_sections(cleaned)
raw_file = paper_dir / "text" / "raw_text.txt"
cleaned_file = paper_dir / "text" / "cleaned_text.txt"
sections_file = paper_dir / "sections" / "sections.json"
structured_prompt = paper_dir / "prompts" / "01_structured_extraction_prompt.md"
verification_prompt = paper_dir / "prompts" / "02_verification_prompt_template.md"
structured_json = paper_dir / "generated" / "structured_result.json"
verification_json = paper_dir / "generated" / "verification_result.json"
final_md = paper_dir / "report" / "final_report.md"
final_html = paper_dir / "report" / "final_report.html"
final_docx = paper_dir / "report" / "final_report.docx"
write_text(raw_file, raw)
write_text(cleaned_file, cleaned)
sections_file.write_text(json.dumps(sections, ensure_ascii=False, indent=2), encoding="utf-8")
write_text(structured_prompt, STRUCTURED_PROMPT_TEMPLATE.format(language=language, **sections))
write_text(verification_prompt, VERIFICATION_PROMPT_TEMPLATE.format(language=language, paper_body=sections["paper_body"]))
if not structured_json.exists():
write_text(structured_json, "{}\n")
if not verification_json.exists():
write_text(verification_json, "{}\n")
return PaperRecord(
id=paper_id,
source_type=source_type,
source=source,
input_file=str(kept),
work_dir=str(paper_dir),
raw_text_file=str(raw_file),
cleaned_text_file=str(cleaned_file),
sections_file=str(sections_file),
structured_prompt_file=str(structured_prompt),
verification_prompt_file=str(verification_prompt),
structured_json_file=str(structured_json),
verification_json_file=str(verification_json),
final_markdown_file=str(final_md),
final_html_file=str(final_html),
final_docx_file=str(final_docx),
language=language,
word_count=word_count,
warning=warning,
)
def main() -> int:
parser = argparse.ArgumentParser(description="Prepare paper files for evidence-enhanced analysis.")
parser.add_argument("--language", default="中文", choices=["中文", "英文"], help="Natural language for generated content.")
parser.add_argument("--files", nargs="*", default=[], help="Local paper files. PDF, DOCX, TXT, MD, HTML are supported.")
parser.add_argument("--urls", nargs="*", default=[], help="Paper PDF/direct URLs. Multiple comma-separated values are also accepted.")
parser.add_argument("--output-root", default=str(desktop_root()), help="Root output directory. Defaults to ~/Desktop/paper_analysis_results.")
args = parser.parse_args()
files = [Path(p).expanduser().resolve() for p in args.files if p]
urls = split_csv(args.urls)
if not files and not urls:
print("上传的文件和论文URL不能同时为空。", file=sys.stderr)
return 2
batch_dir = Path(args.output_root).expanduser() / timestamp()
downloads_dir = batch_dir / "downloads"
batch_dir.mkdir(parents=True, exist_ok=True)
records: list[PaperRecord] = []
index = 1
for file_path in files:
records.append(prepare_one(file_path, "local_file", str(file_path), batch_dir, args.language, index))
index += 1
for url in urls:
downloaded = download_url(url, downloads_dir)
records.append(prepare_one(downloaded, "url", url, batch_dir, args.language, index))
index += 1
manifest = {
"workflow": "paper-analysis-evidence",
"language": args.language,
"created_at": timestamp(),
"batch_dir": str(batch_dir),
"papers": [asdict(r) for r in records],
"next_steps": [
"For each paper, send prompts/01_structured_extraction_prompt.md to the model and save the JSON-only answer to generated/structured_result.json.",
"Then send prompts/02_verification_prompt_template.md plus the structured JSON to the model and save the JSON-only answer to generated/verification_result.json.",
"Run scripts/render_report.py with the manifest to create final_report.md, final_report.html, and final_report.docx.",
],
}
manifest_path = batch_dir / "manifest.json"
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({"manifest": str(manifest_path), "batch_dir": str(batch_dir), "papers": [r.id for r in records]}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:scripts/render_report.py
#!/usr/bin/env python3
"""Render final reports for the evidence-enhanced paper analysis workflow.
Given structured extraction JSON and verification JSON, create Markdown, HTML,
and DOCX reports. This mirrors the Dify Scheme A aggregation node while adding
local file outputs that are easy to inspect on Ubuntu Desktop.
"""
from __future__ import annotations
import argparse
import html
import json
import os
import re
import sys
import zipfile
from pathlib import Path
from typing import Any
def parse_json_text(text: str) -> dict[str, Any]:
if not text:
return {}
s = text.strip()
s = re.sub(r"^```json\s*", "", s)
s = re.sub(r"^```", "", s)
s = re.sub(r"```$", "", s)
s = s.strip()
try:
data = json.loads(s)
return data if isinstance(data, dict) else {"raw_text": data}
except Exception:
match = re.search(r"\{[\s\S]*\}", s)
if match:
try:
data = json.loads(match.group(0))
return data if isinstance(data, dict) else {"raw_text": data}
except Exception:
return {"raw_text": s}
return {"raw_text": s}
def is_english(language: str) -> bool:
return (language or "").strip() == "英文"
def empty_text(language: str) -> str:
return "None" if is_english(language) else "无"
def clean_item(x: Any) -> str:
if isinstance(x, (dict, list)):
return json.dumps(x, ensure_ascii=False)
return str(x).strip()
def list_md(items: Any, language: str) -> str:
fallback = empty_text(language)
if not items:
return fallback
if isinstance(items, list):
lines = [f"- {clean_item(x)}" for x in items if clean_item(x)]
return "\n".join(lines) if lines else fallback
s = clean_item(items)
return s if s else fallback
def results_md(items: Any, language: str) -> str:
fallback = empty_text(language)
if not items or not isinstance(items, list):
return fallback
lines = []
en = is_english(language)
for it in items:
if isinstance(it, dict):
if en:
lines.append(f"- Dataset: {it.get('dataset','')}; Metric: {it.get('metric','')}; Value: {it.get('value','')}; Baseline: {it.get('baseline','')}; Improvement: {it.get('improvement','')}")
else:
lines.append(f"- 数据集:{it.get('dataset','')};指标:{it.get('metric','')};结果:{it.get('value','')};对比基线:{it.get('baseline','')};提升:{it.get('improvement','')}")
else:
s = clean_item(it)
if s:
lines.append(f"- {s}")
return "\n".join(lines) if lines else fallback
def evidence_md(items: Any, language: str) -> str:
fallback = empty_text(language)
if not items or not isinstance(items, list):
return fallback
en = is_english(language)
lines = []
for it in items[:8]:
if isinstance(it, dict):
if en:
lines.append(f"- Field: {it.get('field','')} | Claim: {it.get('claim','')} | Evidence: {it.get('evidence','')}")
else:
lines.append(f"- 字段:{it.get('field','')}|结论:{it.get('claim','')}|证据:{it.get('evidence','')}")
else:
s = clean_item(it)
if s:
lines.append(f"- {s}")
return "\n".join(lines) if lines else fallback
def verify_md(items: Any, language: str) -> str:
fallback = empty_text(language)
if not items or not isinstance(items, list):
return fallback
en = is_english(language)
lines = []
for it in items:
if isinstance(it, dict):
if en:
lines.append(f"- Claim: {it.get('claim','')} | Status: {it.get('status','')} | Evidence: {it.get('evidence','')}")
else:
lines.append(f"- {it.get('claim','')}|状态:{it.get('status','')}|证据:{it.get('evidence','')}")
else:
s = clean_item(it)
if s:
lines.append(f"- {s}")
return "\n".join(lines) if lines else fallback
def issues_md(items: Any, language: str) -> str:
fallback = empty_text(language)
if not items or not isinstance(items, list):
return fallback
en = is_english(language)
lines = []
for it in items:
if isinstance(it, dict):
if en:
lines.append(f"- Field: {it.get('field','')} | Problem: {it.get('problem','')} | Severity: {it.get('severity','')}")
else:
lines.append(f"- 字段:{it.get('field','')}|问题:{it.get('problem','')}|严重度:{it.get('severity','')}")
else:
s = clean_item(it)
if s:
lines.append(f"- {s}")
return "\n".join(lines) if lines else fallback
def build_markdown(structured_json: str, verification_json: str, language: str) -> str:
data = parse_json_text(structured_json)
verify = parse_json_text(verification_json)
en = is_english(language)
title = data.get("title", "Untitled" if en else "未识别题目")
if en:
return f"""# Paper Analysis
## Title
{title}
## Task and Problem
- Task: {data.get('task', '')}
- Background: {data.get('background', '')}
- Problem Statement: {data.get('problem_statement', '')}
## Method Overview
- Method Name: {data.get('method_name', '')}
- Method Core: {data.get('method_core', '')}
## Experimental Elements
### Datasets
{list_md(data.get('datasets', []), language)}
### Baselines
{list_md(data.get('baselines', []), language)}
### Metrics
{list_md(data.get('metrics', []), language)}
## Main Results
{results_md(data.get('main_results', []), language)}
## Contributions
{list_md(data.get('contributions', []), language)}
## Ablations and Limitations
### Ablations
{list_md(data.get('ablations', []), language)}
### Limitations
{list_md(data.get('limitations', []), language)}
## Evidence Spans
{evidence_md(data.get('evidence_spans', []), language)}
## Consistency Check
- Overall Score: {verify.get('overall_score', '')}
- Hallucination Risk: {verify.get('hallucination_risk', '')}
- Final Verdict: {verify.get('final_verdict', '')}
### Verified Claims
{verify_md(verify.get('verified_claims', []), language)}
### Issues
{issues_md(verify.get('issues', []), language)}
"""
return f"""# 论文分析结果
## 论文题目
{title}
## 任务与问题
- 研究任务:{data.get('task', '')}
- 背景:{data.get('background', '')}
- 问题定义:{data.get('problem_statement', '')}
## 方法概述
- 方法名称:{data.get('method_name', '')}
- 方法核心:{data.get('method_core', '')}
## 实验要素
### 数据集
{list_md(data.get('datasets', []), language)}
### 基线方法
{list_md(data.get('baselines', []), language)}
### 评价指标
{list_md(data.get('metrics', []), language)}
## 主要结果
{results_md(data.get('main_results', []), language)}
## 贡献提炼
{list_md(data.get('contributions', []), language)}
## 消融与局限性
### 消融实验
{list_md(data.get('ablations', []), language)}
### 局限性
{list_md(data.get('limitations', []), language)}
## 证据片段
{evidence_md(data.get('evidence_spans', []), language)}
## 一致性校验
- 总评分:{verify.get('overall_score', '')}
- 幻觉风险:{verify.get('hallucination_risk', '')}
- 最终结论:{verify.get('final_verdict', '')}
### 已核验结论
{verify_md(verify.get('verified_claims', []), language)}
### 发现的问题
{issues_md(verify.get('issues', []), language)}
"""
def markdown_to_html(markdown: str, title: str = "Paper Analysis") -> str:
try:
import markdown as markdown_lib # type: ignore
body = markdown_lib.markdown(markdown, extensions=["tables", "fenced_code", "sane_lists"])
except Exception:
body_lines: list[str] = []
in_ul = False
for raw in markdown.splitlines():
line = raw.rstrip()
if line.startswith("### "):
if in_ul:
body_lines.append("</ul>"); in_ul = False
body_lines.append(f"<h3>{html.escape(line[4:])}</h3>")
elif line.startswith("## "):
if in_ul:
body_lines.append("</ul>"); in_ul = False
body_lines.append(f"<h2>{html.escape(line[3:])}</h2>")
elif line.startswith("# "):
if in_ul:
body_lines.append("</ul>"); in_ul = False
body_lines.append(f"<h1>{html.escape(line[2:])}</h1>")
elif line.startswith("- "):
if not in_ul:
body_lines.append("<ul>"); in_ul = True
body_lines.append(f"<li>{html.escape(line[2:])}</li>")
elif not line.strip():
if in_ul:
body_lines.append("</ul>"); in_ul = False
else:
if in_ul:
body_lines.append("</ul>"); in_ul = False
body_lines.append(f"<p>{html.escape(line)}</p>")
if in_ul:
body_lines.append("</ul>")
body = "\n".join(body_lines)
return f"""<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{html.escape(title)}</title>
<style>
body {{ max-width: 920px; margin: 40px auto; padding: 0 24px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.65; color: #1f2937; }}
h1, h2, h3 {{ color: #111827; line-height: 1.25; }}
h1 {{ border-bottom: 2px solid #e5e7eb; padding-bottom: 12px; }}
h2 {{ margin-top: 32px; border-bottom: 1px solid #e5e7eb; padding-bottom: 6px; }}
li {{ margin: 6px 0; }}
code, pre {{ background: #f3f4f6; }}
</style>
</head>
<body>
{body}
</body>
</html>
"""
def write_docx_with_python_docx(markdown: str, output: Path) -> bool:
try:
from docx import Document # type: ignore
except Exception:
return False
doc = Document()
for raw in markdown.splitlines():
line = raw.rstrip()
if not line:
continue
if line.startswith("# "):
doc.add_heading(line[2:].strip(), level=1)
elif line.startswith("## "):
doc.add_heading(line[3:].strip(), level=2)
elif line.startswith("### "):
doc.add_heading(line[4:].strip(), level=3)
elif line.startswith("- "):
doc.add_paragraph(line[2:].strip(), style="List Bullet")
else:
doc.add_paragraph(line)
doc.save(output)
return True
def write_minimal_docx(markdown: str, output: Path) -> None:
def xml_escape(s: str) -> str:
return html.escape(s, quote=False)
paras = []
for raw in markdown.splitlines():
line = raw.strip()
if not line:
continue
if line.startswith("#"):
line = line.lstrip("#").strip()
elif line.startswith("- "):
line = "• " + line[2:].strip()
paras.append(f"<w:p><w:r><w:t>{xml_escape(line)}</w:t></w:r></w:p>")
document_xml = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>%s<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/></w:sectPr></w:body></w:document>""" % "".join(paras)
content_types = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>"""
rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"""
output.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr("[Content_Types].xml", content_types)
zf.writestr("_rels/.rels", rels)
zf.writestr("word/document.xml", document_xml)
def write_docx(markdown: str, output: Path) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
if not write_docx_with_python_docx(markdown, output):
write_minimal_docx(markdown, output)
def render_one(structured_path: Path, verification_path: Path, language: str, output_md: Path, output_html: Path, output_docx: Path) -> dict[str, str]:
structured = structured_path.read_text(encoding="utf-8", errors="ignore")
verification = verification_path.read_text(encoding="utf-8", errors="ignore")
markdown = build_markdown(structured, verification, language)
output_md.parent.mkdir(parents=True, exist_ok=True)
output_md.write_text(markdown, encoding="utf-8")
output_html.write_text(markdown_to_html(markdown), encoding="utf-8")
write_docx(markdown, output_docx)
return {"markdown": str(output_md), "html": str(output_html), "docx": str(output_docx)}
def render_manifest(manifest_path: Path) -> list[dict[str, str]]:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
outputs: list[dict[str, str]] = []
language = manifest.get("language", "中文")
for paper in manifest.get("papers", []):
result = render_one(
Path(paper["structured_json_file"]),
Path(paper["verification_json_file"]),
paper.get("language") or language,
Path(paper["final_markdown_file"]),
Path(paper["final_html_file"]),
Path(paper["final_docx_file"]),
)
outputs.append({"paper_id": paper.get("id", ""), **result})
return outputs
def main() -> int:
parser = argparse.ArgumentParser(description="Render paper analysis reports from generated JSON files.")
parser.add_argument("--manifest", help="Manifest created by prepare_papers.py. If set, per-paper paths are read from the manifest.")
parser.add_argument("--structured-json", help="Path to structured_result.json for single-paper mode.")
parser.add_argument("--verification-json", help="Path to verification_result.json for single-paper mode.")
parser.add_argument("--language", default="中文", choices=["中文", "英文"])
parser.add_argument("--output-md", help="Output Markdown path for single-paper mode.")
parser.add_argument("--output-html", help="Output rendered HTML path for single-paper mode.")
parser.add_argument("--output-docx", help="Output DOCX path for single-paper mode.")
args = parser.parse_args()
if args.manifest:
outputs = render_manifest(Path(args.manifest).expanduser())
print(json.dumps({"outputs": outputs}, ensure_ascii=False, indent=2))
return 0
required = [args.structured_json, args.verification_json, args.output_md, args.output_html, args.output_docx]
if not all(required):
parser.error("Either --manifest or all single-paper paths are required.")
result = render_one(
Path(args.structured_json).expanduser(),
Path(args.verification_json).expanduser(),
args.language,
Path(args.output_md).expanduser(),
Path(args.output_html).expanduser(),
Path(args.output_docx).expanduser(),
)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:references/prompt_templates.md
# Prompt templates
These templates are adapted from the uploaded Dify DSL `论文分析系统_方案A_结构化证据增强版`.
## Structured extraction prompt
Use after `prepare_papers.py` has created section files. The script writes a filled prompt to `prompts/01_structured_extraction_prompt.md`; prefer the filled file over manually copying this template.
```text
你是一位严谨的论文信息抽取器。请严格基于给定论文内容,输出一个 JSON 对象,不要输出 Markdown、不要解释、不要加 ```json。
输出字段必须包含:
{
"title": "",
"task": "",
"background": "",
"problem_statement": "",
"method_name": "",
"method_core": "",
"datasets": [],
"baselines": [],
"metrics": [],
"main_results": [
{"dataset": "", "metric": "", "value": "", "baseline": "", "improvement": ""}
],
"ablations": [],
"limitations": [],
"claims": [],
"contributions": [],
"evidence_spans": [
{"field": "", "claim": "", "evidence": ""}
]
}
规则:
1. 只能写原文出现或可直接推出的信息,不要脑补。
2. 优先使用对应章节抽取信息;如果某章节为空或信息不足,必须从全文补充。
3. datasets、baselines、metrics 不允许因为章节为空而直接写空数组,必须先检查全文 paper_body、实验结果、实现细节、表格附近文本是否存在相关信息。
4. 只有在全文中也确实找不到时,才能写空字符串或空数组。
5. evidence_spans 至少给 6 条,每条都必须是原文中的直接证据片段或非常贴近原文的概括。
6. 数值结果优先来自实验部分、结果部分、分析部分或表格附近文本。
7. JSON 键名保持英文;JSON 中的自然语言内容使用用户选择的语言。
```
## Verification prompt
Use only after the structured JSON exists. The script writes a filled template to `prompts/02_verification_prompt_template.md`; paste the actual `structured_result.json` into the `{{structured_json}}` placeholder before sending.
```text
你是一位论文事实核验器。请对下面的结构化抽取结果做一致性校验,并输出 JSON 对象,不要输出 Markdown、不要解释。
输出格式:
{
"overall_score": 0,
"hallucination_risk": "low/medium/high",
"issues": [
{"field": "", "problem": "", "severity": "low/medium/high"}
],
"verified_claims": [
{"claim": "", "status": "supported/weak/unsupported", "evidence": ""}
],
"final_verdict": ""
}
评分规则:
- 5:几乎无幻觉,证据充分
- 4:少量不严谨
- 3:有若干缺证据表述
- 2:存在明显不一致
- 1:大量幻觉或错读
要求:
1. JSON 键名保持英文。
2. JSON 中的自然语言内容使用用户选择的语言。
3. 重点检查 datasets、baselines、metrics、main_results 是否遗漏或误抽。
4. 若抽取结果将原文存在的信息写成空数组或空字符串,请在 issues 中明确指出。
5. verified_claims 至少给 4 条。
```
FILE:references/workflow_mapping.md
# Dify Scheme A to OpenClaw Skill mapping
## Original Dify intent
The uploaded workflow is named `论文分析系统_方案A_结构化证据增强版`. It analyzes one or more uploaded papers and/or paper PDF URLs, extracts structured paper information, performs evidence-focused consistency verification, and exports a Markdown-to-DOCX report.
## Node mapping
| Dify node | Skill implementation |
|---|---|
| `if_empty` + `empty_error` | `prepare_papers.py` exits with `上传的文件和论文URL不能同时为空。` when no inputs are provided. |
| `split_urls` | `prepare_papers.py --urls` accepts repeated values or comma-separated URL lists. |
| `http_download` | `prepare_papers.py` downloads URLs to the batch `downloads/` directory under the Desktop output root. |
| `upload_doc_extract` / `download_doc_extract` | `prepare_papers.py` extracts text from copied local files or downloaded files. |
| `upload_clean` / `download_clean` | `prepare_papers.py` applies the same whitespace normalization and reference/bibliography trimming pattern. |
| `upload_sections` / `download_sections` | `prepare_papers.py` implements the same abstract, intro, method, experiment, conclusion, and `paper_body` section split. |
| `upload_structured` / `download_structured` | The Agent must send `prompts/01_structured_extraction_prompt.md` to the model and save JSON-only output to `generated/structured_result.json`. |
| `upload_verify` / `download_verify` | The Agent must send `prompts/02_verification_prompt_template.md` with actual structured JSON inserted and save JSON-only output to `generated/verification_result.json`. |
| `upload_render` / `download_render` | `render_report.py` aggregates structured and verification JSON into a final report. |
| `upload_docx` / `download_docx` | `render_report.py` writes `.docx`; it also writes `.md` and rendered `.html` for easier Markdown preview. |
## Output directory convention
Default runtime path:
```text
~/Desktop/paper_analysis_results/<YYYYMMDD_HHMMSS>/
```
Per-paper files are placed under:
```text
paper_XX_<filename>/
├── input/
├── text/raw_text.txt
├── text/cleaned_text.txt
├── sections/sections.json
├── prompts/01_structured_extraction_prompt.md
├── prompts/02_verification_prompt_template.md
├── generated/structured_result.json
├── generated/verification_result.json
└── report/
├── final_report.md
├── final_report.html
└── final_report.docx
```
FILE:agents/openai.yaml
interface:
display_name: "Paper Analysis Evidence"
short_description: "Extracts evidence-backed structured paper analysis and consistency checks."
icon: "📄"
color: "#FFEAD5"
Workflow-driven skill that autonomously acts as an executive assistant to block out calendar time for incomplete tasks based on urgency and estimated duration.
---
name: executive-assistant-time-blocking
description: Workflow-driven skill that autonomously acts as an executive assistant to block out calendar time for incomplete tasks based on urgency and estimated duration.
os: all
requires:
bins: [gog]
---
## Lean Philosophy (Principles)
- **Kaizen (改善):** This skill orchestrates multiple atomic nodes (tasks retrieval, cognitive assessment, calendar retrieval, gap analysis, and event creation) into a cohesive workflow, ensuring each step is validated before proceeding.
- **Standardized Work (Hyojun Sagyo):** This node represents the standardized standard operating procedure (SOP) for automated time-blocking and scheduling of user tasks.
- **Jidoka (自働化):** This workflow includes autonomous defect detection. It will check for calendar overlaps, verify event creation, and repeat scheduling steps if a slot is double-booked or an event fails to create.
# Executive Assistant Time Blocking
This skill directs the agent to evaluate current incomplete tasks from Google Tasks, estimate their duration and urgency, and sequentially schedule them into available gaps in Google Calendar.
## Cognitive Directives
WHEN [Requested to schedule tasks, time-block the backlog, or act as an executive assistant for schedule management]
THEN [
Execute the following Jidoka-validated loop:
1. **Task Collection:** Execute the native terminal command `gog tasks list @default --json` to retrieve all current incomplete tasks.
- **Verification Step (Jidoka):** Check if the returned output is a valid JSON array of tasks. IF it fails or errors, report the error and STOP. IF empty, report that there are no tasks to schedule and STOP.
2. **Cognitive Assessment (Triage):**
- For each task, autonomously estimate the time required to complete it.
- Assess the urgency of each task.
- Judge when the task should be scheduled (e.g., today vs. another day soon).
- **Verification Step (Jidoka):** Verify that *every* collected task has been assigned an estimated duration, an urgency score, and a target date. IF any task is missing these values, correct the assessment and retry before proceeding.
3. **Schedule Retrieval:**
- For the target days identified, execute `gog calendar events primary --from "<start_date_iso>" --to "<end_date_iso>" --json` to retrieve currently booked events.
- **Verification Step (Jidoka):** Verify the command returns a valid JSON array of events. IF it returns an error (e.g., rate limit or network error), wait 3 seconds and retry (max 3 times). IF it still fails, report the error and STOP.
4. **Gap Analysis:**
- Analyze the retrieved calendar events to identify gaps in the schedule.
- **Constraint Check:** Ensure that gaps respect standard human needs (e.g., leave appropriate blocks of time for eating meals and sleeping).
- **Verification Step (Jidoka):** Verify that the calculated gaps for a target day are large enough to fit the assigned tasks. IF there is insufficient time on the target day, reassign the overflow tasks to the next day, and repeat Step 3 (Schedule Retrieval) for the new target day.
5. **Sequential Time Blocking (Jidoka Loop):**
- Sequentially fit tasks into the schedule gaps based on their estimated duration and urgency.
- For each fitted task, execute `gog calendar create primary --summary "[Task] <Task Title>" --from "<start_time_iso>" --to "<end_time_iso>" --json`.
- **Verification Step:** After creation, execute `gog calendar events primary --from "<start_time_iso>" --to "<end_time_iso>" --json` again for that time slot to verify the event was correctly created and recalculate remaining gaps.
6. **Overlap Audit & Remediation:**
- Once all tasks have been assigned a block, perform a final audit using `gog calendar events` across all modified days.
- Double-check that there are no overlaps or double-booked slots.
- IF double-booking is detected: Delete or reschedule the conflicting task event (`gog calendar delete primary <eventId>`) and repeat the scheduling steps (Steps 3-5) until all tasks are booked without conflict.
]
## Expected Output
A comprehensive JSON or Markdown summary of the scheduled tasks, detailing the timeline, any constraints respected, and confirmation of overlap-free scheduling.
Workflow-driven skill that autonomously detects cancelled events in emails and syncs the calendar state by deleting or updating orphaned calendar blocks.
---
name: Event-Cancellation-Reconciler
description: Workflow-driven skill that autonomously detects cancelled events in emails and syncs the calendar state by deleting or updating orphaned calendar blocks.
os: all
requires:
bins:
- gog
---
## Lean Philosophy (Principles)
- **Kaizen (改善):** This skill is broken down into atomic steps: parsing the email intent, locating the stale event, and reconciling the state.
- **Standardized Work (Hyojun Sagyo):** This node represents the standardized workflow for state-syncing Google Calendar based on Gmail cancellation notices.
- **Jidoka (自働化):** Includes autonomous self-healing loops. If no event is found during the search phase, the agent stops and gracefully reports completion.
# Event Cancellation Reconciler
This skill orchestrates multiple tools to automatically remove or update calendar events when an email indicates a cancellation or reschedule, preventing stale state and double-booking.
## Cognitive Directives
WHEN [Requested to handle a cancelled event from an email OR when reading an email indicating a cancellation/reschedule]
THEN [
Execute the following Jidoka-validated loop:
1. Extract Event Details: Use the `LLM-Extract-JSON` or `LLM-Extract-Action-Items` skill against the email body to extract the `original_date`, `original_time`, and `target_entity` (event subject).
- **Verification Step (Jidoka):** Verify the dates and subject are extracted. IF extraction fails, ask the user for the event details and STOP.
2. Locate Orphaned Event: Execute the native terminal command `gog calendar search "<target_entity>" --json` or `gog calendar events primary --from "<original_date>T00:00:00Z" --to "<original_date>T23:59:59Z" --json` to locate the event ID.
- **Verification Step (Jidoka):** Check if any events are returned. IF no matching event is found, reply "No conflicting calendar events found to clean up" and STOP.
3. Reconcile State: Execute `gog calendar delete <event_id>` to clear the block, or optionally `gog calendar update <event_id> --summary "[CANCELLED] <original_title>"` to preserve a record.
- **Verification Step (Jidoka):** Verify the native CLI returns a success response. IF it fails, log the failure and attempt to retry once before prompting the user.
4. Notify: Reply to the user stating the cancellation has been processed and the calendar block is cleared.
]
## Expected Output
A confirmation message stating the calendar has been synced and the specific event has been deleted or marked as cancelled.
Make real phone calls, handle inbound and outbound calls, and check call status with Call-E. Schedule calls, run batch calling tasks, and get call results wi...
---
name: Phone Calls — Call-E
description: Make real phone calls, handle inbound and outbound calls, and check call status with Call-E. Schedule calls, run batch calling tasks, and get call results with transcripts. Supports international calling beyond +1 regions.
license: MIT-0
metadata:
openclaw:
requires:
bins:
- openclaw
- node
---
# Phone Calls — Call-E
🎉 Includes 20 free phone calls — no setup cost to try real calling.
Make real phone calls, handle inbound and outbound calls, and check call status using Call-E.
Call-E supports scheduled calls, batch calling workflows, and provides detailed call results with transcripts. It also supports international calling beyond +1 regions.
Use this skill when the user wants to:
- make a phone call
- call a phone number
- place an outbound call
- receive or handle inbound calls
- call a business or customer
- follow up by phone
- continue an active call
- check call status
This skill handles two things as part of its normal purpose:
1. Prepare the local OpenClaw environment so the `calle` plugin is available.
2. Teach the agent how to use the `calle_*` tools correctly once the plugin is
available.
* * *
## Safety and consent rules
- Installing this plugin is an external software installation.
- Restarting the OpenClaw gateway is a privileged local operation.
- Real phone calls can contact external people or businesses and may create
cost, privacy, or compliance implications.
- Do not place a real call unless the user clearly intends to do so.
- Do not guess phone numbers, country codes, region, or language.
- If the user only wants a script, wording help, roleplay, or a simulated
dialogue, do not use the plugin tools.
* * *
## Trigger phrases
Use this skill when the user expresses intent such as:
- "call this number"
- "make a phone call"
- "call the business"
- "call the customer"
- "place an outbound call"
- "follow up by phone"
- "check the status of that call"
* * *
## When to use this skill
Use this skill when the user wants to:
- install or enable the Call-E plugin
- place a real outbound phone call
- continue a call workflow that uses Call-E
- check the status of a call that has already started
- recover from a missing-plugin situation before making a call
This skill is especially appropriate when the user says they want to make a
phone call directly and the agent should prefer the Call-E workflow instead of
searching broadly across unrelated capabilities.
* * *
## When not to use this skill
Do not use this skill for:
- writing a call script only
- simulated conversations or rehearsal
- general contact lookup that does not require placing a call
- unrelated OpenClaw troubleshooting outside the scope of the Call-E plugin
* * *
## Prerequisite
This skill depends on the Call-E OpenClaw plugin.
If the plugin is missing, install it with:
`bash scripts/openclaw-setup.sh`
This is the preferred install path when the installed skill bundle includes the
packaged setup script locally.
The script installs the published plugin package, enables `calle`, merges the
required OpenClaw config, and may prompt to restart the gateway.
If the packaged script is unavailable in the current environment, run:
`curl -fsSL https://raw.githubusercontent.com/CALLE-AI/call-e-integrations/main/openclaw-setup.sh | bash`
This preserves the previous remote install path for environments that only have
the skill instructions and no packaged script file on disk.
If `curl` is unavailable or the user prefers the manual path, run:
`openclaw plugins install @call-e/openagent`
Then enable the plugin:
`openclaw plugins enable calle`
Then restart the gateway if needed:
`openclaw gateway restart`
If the current session still does not see the plugin tools after restart,
retry the same request in a new session.
* * *
## What gets installed
This setup installs the published Call-E OpenClaw plugin and prepares the
local gateway to load it.
Expected tools after setup:
- `calle_plan_call`
- `calle_run_call`
- `calle_get_call_run`
Source repository:
- https://github.com/CALLE-AI/call-e-integrations
* * *
## Definition of Done
This task is not complete until all of the following are true:
1. the `calle` plugin is installed
2. the plugin is enabled
3. the OpenClaw gateway has been restarted if needed
4. the Call-E tools are available in the current environment, or the user has
been clearly told to retry after restart
5. if the user wanted to place a call, the agent proceeds through the correct
Call-E tool flow
* * *
## Install flow
### Step 1 - Check plugin availability
Prefer using `openclaw plugins list` to determine whether `calle` is already
installed.
If `calle` is already present, do not reinstall it unless the user explicitly
asks to reinstall or repair setup.
### Step 2 - Install and enable plugin if needed
If the plugin is missing, run:
`bash scripts/openclaw-setup.sh`
Use this as the default install command when the packaged skill script exists in
the local skill directory.
The script already installs the published plugin package, enables `calle`,
merges the required OpenClaw config, and may prompt to restart the gateway.
If the packaged script is unavailable, run:
`curl -fsSL https://raw.githubusercontent.com/CALLE-AI/call-e-integrations/main/openclaw-setup.sh | bash`
This keeps the previous remote install path intact.
If `curl` is unavailable or the user wants the manual path instead, run:
`openclaw plugins install @call-e/openagent`
Then run:
`openclaw plugins enable calle`
Use `curl` only for installation or repair of the plugin. Once the plugin tools
are available in the session, do not use `curl`, raw HTTP, or shell commands to
perform real Call-E call actions.
### Step 3 - Restart gateway if needed
If the manual install path was used, or if the script skipped restart, run:
`openclaw gateway restart`
Then tell the user to retry the same request if the current session has not
picked up the plugin yet.
### Step 4 - Verify readiness
A successful setup should make these tools available:
- `calle_plan_call`
- `calle_run_call`
- `calle_get_call_run`
If those tools are not yet available, do not proceed with call execution.
* * *
## Tool flow
Once the plugin is available, use the tools in this order.
### 1. Plan first
Always start with `calle_plan_call`.
Pass the user's latest request in `user_input`.
Only provide structured fields such as `goal`, `language`, `region`, or
`to_phones` when they are explicitly known.
Do not invent or normalize uncertain phone numbers or locale details.
### 2. Run only after planning is ready
Use `calle_run_call` only after planning returns a valid `plan_id` and
`confirm_token`.
Use those values exactly as returned.
Do not start the call unless the user clearly wants to proceed.
### 3. Check status only for an existing call
Use `calle_get_call_run` only when a call has already started and a valid
`run_id` exists.
Summarize the status clearly for the user.
* * *
## Authentication flow
If a Call-E tool returns authentication requirements:
- check for `auth_required`
- check for `login_url`
When present:
1. tell the user to open the browser link
2. ask them to complete login
3. retry the same tool call after login completes
Do not switch to a different tool or invent fallback behavior for protected
actions.
* * *
## Notes for the agent
- Prefer the Call-E workflow quickly when the user clearly means a real phone
call.
- Treat plugin setup as part of the normal workflow, not a separate advanced
task.
- If setup changed the local environment, be explicit that the gateway may
need a restart before tools appear.
- Keep user-facing explanations short: install if needed, authenticate if
needed, then place or inspect the call.
- If execution is blocked because the local environment cannot run commands,
provide either `bash scripts/openclaw-setup.sh` or
`curl -fsSL https://raw.githubusercontent.com/CALLE-AI/call-e-integrations/main/openclaw-setup.sh | bash`,
depending on which install path is actually available, and explain the next
step briefly.
FILE:scripts/openclaw-setup.sh
#!/usr/bin/env bash
set -euo pipefail
PACKAGE_NAME="@call-e/openagent"
PLUGIN_ID="calle"
OPENCLAW_CONFIG_PATH="HOME/.openclaw/openclaw.json"
log() {
printf '[openclaw-setup] %s\n' "$*"
}
fail() {
printf '[openclaw-setup] %s\n' "$*" >&2
exit 1
}
usage() {
cat <<'EOF'
Usage: ./openclaw-setup.sh
Installs the published OpenClaw plugin package, enables `calle`, merges the
required OpenClaw config, and optionally restarts the gateway after prompting.
EOF
}
require_command() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
fail "Required command not found: cmd"
fi
}
plugin_already_installed() {
local list_output
if ! list_output="$(openclaw plugins list 2>/dev/null)"; then
return 1
fi
if printf '%s\n' "$list_output" | grep -Eq '(^|[[:space:][:punct:]])calle([[:space:][:punct:]]|$)|@call-e/openagent'; then
return 0
fi
return 1
}
merge_openclaw_config() {
mkdir -p "$(dirname "$OPENCLAW_CONFIG_PATH")"
OPENCLAW_CONFIG_PATH="$OPENCLAW_CONFIG_PATH" PLUGIN_ID="$PLUGIN_ID" node <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const configPath = process.env.OPENCLAW_CONFIG_PATH;
const pluginId = process.env.PLUGIN_ID;
const toolIds = [
"calle_plan_call",
"calle_run_call",
"calle_get_call_run",
];
function fail(message) {
console.error(`[openclaw-setup] message`);
process.exit(1);
}
function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
let data = {};
if (fs.existsSync(configPath)) {
const raw = fs.readFileSync(configPath, "utf8").trim();
if (raw.length > 0) {
try {
data = JSON.parse(raw);
} catch (error) {
fail(`Failed to parse configPath: error.message`);
}
}
}
if (!isObject(data)) {
fail(`configPath must contain a JSON object at the root.`);
}
const plugins = data.plugins;
if (plugins !== undefined && !isObject(plugins)) {
fail(`configPath field "plugins" must be a JSON object.`);
}
const nextPlugins = isObject(plugins) ? { ...plugins } : {};
const entries = nextPlugins.entries;
if (entries !== undefined && !isObject(entries)) {
fail(`configPath field "plugins.entries" must be a JSON object.`);
}
const nextEntries = isObject(entries) ? { ...entries } : {};
const existingEntry = nextEntries[pluginId];
if (existingEntry !== undefined && !isObject(existingEntry)) {
fail(`configPath field "plugins.entries.pluginId" must be a JSON object.`);
}
nextEntries[pluginId] = {
...(isObject(existingEntry) ? existingEntry : {}),
enabled: true,
};
const allow = nextPlugins.allow;
if (allow !== undefined && !Array.isArray(allow)) {
fail(`configPath field "plugins.allow" must be an array.`);
}
const nextAllow = Array.isArray(allow) ? [...allow] : [];
if (!nextAllow.includes(pluginId)) {
nextAllow.push(pluginId);
}
nextPlugins.entries = nextEntries;
nextPlugins.allow = nextAllow;
data.plugins = nextPlugins;
const tools = data.tools;
if (tools !== undefined && !isObject(tools)) {
fail(`configPath field "tools" must be a JSON object.`);
}
const nextTools = isObject(tools) ? { ...tools } : {};
const alsoAllow = nextTools.alsoAllow;
if (alsoAllow !== undefined && !Array.isArray(alsoAllow)) {
fail(`configPath field "tools.alsoAllow" must be an array.`);
}
const nextAlsoAllow = Array.isArray(alsoAllow) ? [...alsoAllow] : [];
for (const toolId of toolIds) {
if (!nextAlsoAllow.includes(toolId)) {
nextAlsoAllow.push(toolId);
}
}
nextTools.alsoAllow = nextAlsoAllow;
data.tools = nextTools;
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `JSON.stringify(data, null, 2)\n`);
NODE
}
prompt_restart() {
local prompt_input
prompt_input="/dev/stdin"
if [ -r /dev/tty ]; then
prompt_input="/dev/tty"
elif [ ! -t 0 ]; then
log "No interactive terminal detected. Skipped gateway restart."
log "Run \`openclaw gateway restart\` manually when you are ready."
return 0
fi
local answer
read -r -p "Restart openclaw gateway now? [y/N] " answer <"$prompt_input" || true
case "$answer" in
[Yy]|[Yy][Ee][Ss])
log "Restarting openclaw gateway"
openclaw gateway restart
;;
*)
log "Skipped gateway restart"
log "Run \`openclaw gateway restart\` manually when you are ready."
;;
esac
}
main() {
if [ "-" = "--help" ]; then
usage
exit 0
fi
if [ "$#" -ne 0 ]; then
usage >&2
exit 1
fi
require_command openclaw
require_command node
if plugin_already_installed; then
log "PLUGIN_ID already appears in \`openclaw plugins list\`; skipping install"
else
log "Installing PACKAGE_NAME"
if ! openclaw plugins install "$PACKAGE_NAME"; then
cat >&2 <<'EOF'
[openclaw-setup] Install failed.
[openclaw-setup] If the error mentions `429 Rate limit exceeded`, configure
[openclaw-setup] ~/.config/clawhub/config.json with a valid ClawHub access token
[openclaw-setup] and rerun this script.
EOF
exit 1
fi
fi
log "Enabling PLUGIN_ID"
openclaw plugins enable "$PLUGIN_ID"
log "Merging OPENCLAW_CONFIG_PATH"
merge_openclaw_config
prompt_restart
log "Installed plugins"
openclaw plugins list
cat <<'EOF'
[openclaw-setup] Next checks:
[openclaw-setup] 1. Open OpenClaw Control UI and inspect `Tools -> Available Right Now`.
[openclaw-setup] 2. Confirm `calle_plan_call`, `calle_run_call`, and `calle_get_call_run` are present.
[openclaw-setup] 3. Trigger one protected tool call to start the browser login flow.
EOF
}
main "$@"
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())
Default token-efficient assistant discipline. Use by default for all suitable tasks unless the user explicitly asks for exhaustive detail, teaching-style exp...
---
name: zerotoken-skill
description: Default token-efficient assistant discipline. Use by default for all suitable tasks unless the user explicitly asks for exhaustive detail, teaching-style explanation, brainstorming, or broad exploration. Also use when the user asks to save tokens, reduce context usage, be concise, avoid verbose reasoning, optimize prompt or workflow cost, summarize large context compactly, craft minimal precise prompts, or complete coding/research tasks with minimal necessary reading and output. Prioritize task completion with strict context budgeting, precise prompt framing, progressive disclosure, short answers, and no unnecessary restatement.
metadata:
version: 1.0.3
---
# ZeroToken Skill
默认用最少必要 token 和最精准提示词完成任务。省 token 不等于偷工减料;核心是减少无效上下文、无效解释、无效工具调用和无效输出。
## 默认触发
除非用户明确要求详尽解释、教学式展开、头脑风暴或广泛探索,否则默认使用 ZeroToken 工作方式。
## 核心原则
- 先做任务分类,再决定上下文预算。
- 把用户目标压缩成最少、最精准的可执行提示词:目标、输入、约束、输出格式。
- 只读完成任务必需的材料;先搜定位,再局部读取。
- 输出先给结论和可执行结果;解释按需补充。
- 不复述用户已知内容,不写礼貌性铺垫,不做空泛总结。
- 不展示长推理链;给关键依据、假设、风险即可。
- 能用文件名、行号、命令、列表表达的,不写长段落。
## 精准提示词
当用户的问题含糊、过长或需要转交给模型/Agent 执行时,先提炼最短有效提示词:
- 明确任务目标:要解决什么问题。
- 保留必要输入:数据、代码、错误、上下文位置。
- 写清约束:不要做什么、必须满足什么。
- 固定输出:格式、字段、长度、验收标准。
- 删除无关背景、情绪化描述和重复条件。
若用户要的是“帮我解决问题”,直接用提炼后的提示词推动执行;只有在缺少关键输入会导致结果不可用时才提问。
## 任务分级
### A. 简单问答
用于定义、翻译、改写、命令解释、单点事实、短建议。
- 直接回答。
- 默认 1-5 句话。
- 不列计划,不问澄清,除非缺少关键对象。
- 不主动扩展背景。
### B. 代码小改
用于单文件或局部修复、简单配置、文案调整。
- 先用 `rg` 定位相关文件。
- 只读命中的邻近代码和必要配置。
- 修改后运行最小相关验证。
- 最终只说明改了什么、验证结果、未验证原因。
### C. 多文件任务
用于跨模块功能、测试修复、重构、CI 问题。
- 先列 3-5 步短计划。
- 每步只加载当前决策需要的文件。
- 把长发现压缩成事实清单。
- 避免把所有上下文一次性塞进回答。
### D. 大资料总结
用于长文、日志、网页、PR、需求文档、会议记录。
- 先识别用户要的输出类型:摘要、决策、风险、待办、差异、时间线。
- 不逐段复述。
- 保留数字、日期、负责人、结论、阻塞点。
- 用“要点 + 证据位置”代替大段引用。
## 上下文读取规则
- 优先 `rg` / 文件列表 / 目录结构定位,不先打开大文件。
- 对长文件先读取目录、标题、函数名、导出项或命中片段。
- 只在需要修改、验证或引用时读取完整文件。
- 对重复模式,只读 1-2 个代表样本。
- 已经得到足够信息时停止继续探索。
## 输出压缩规则
默认采用以下顺序:
1. 结论或完成状态
2. 关键变更或答案
3. 验证结果
4. 必要风险或下一步
避免:
- “下面是详细说明”后跟长背景。
- 重复用户问题。
- 解释常识性工具或语法。
- 同义词堆叠。
- 无行动价值的免责声明。
## 编码任务格式
最终回答优先使用短格式:
```text
已完成:...
改动:...
验证:...
注意:...
```
若没有风险或注意事项,省略 `注意`。
## 研究任务格式
默认使用:
```text
结论:...
依据:...
不确定:...
下一步:...
```
时间敏感、法律、医疗、金融、产品价格、API 最新规则等问题仍按宿主规则浏览或核验;不要为了省 token 牺牲准确性。
## 澄清问题
只在以下情况提问:
- 缺少关键输入会导致结果不可用。
- 多个合理目标会产生完全不同产物。
- 操作可能破坏用户数据或造成明显成本。
提问时最多问 1 个问题。能做合理假设时先做,并在答案中标明假设。
## 工具使用
- 能并行读取独立文件时并行。
- 不为展示过程而运行工具。
- 不运行与最终答案无关的验证。
- 命令失败后,只保留关键错误和下一步判断。
## 质量底线
- 不省略安全、准确性和用户明确要求。
- 不跳过必要测试来制造“省 token”的假象。
- 不用短答案掩盖不确定性。
- 不把猜测写成事实。
FILE:agents/openai.yaml
interface:
display_name: "ZeroToken"
short_description: "用最少必要上下文和输出完成任务"
default_prompt: "请用 ZeroToken 方式处理:用最少最精准的提示词提炼目标、输入、约束和输出格式;先完成任务,只保留必要上下文、关键依据和最短可执行输出。"
FILE:CHANGELOG.md
# Changelog
All notable changes to this project will be documented in this file.
## [1.0.3] - 2026-04-27
### Changed
- Expanded package keywords for prompt engineering, context optimization, token budgeting, and agent workflow discovery.
## [1.0.2] - 2026-04-27
### Added
- Added guidance for crafting the shortest precise prompt needed to solve the user's problem.
- Added prompt framing rules for goal, input, constraints, output format, and acceptance criteria.
## [1.0.1] - 2026-04-27
### Changed
- Changed the skill trigger guidance so ZeroToken is the default working discipline for suitable tasks.
- Documented exceptions for exhaustive explanation, teaching-style expansion, brainstorming, and broad exploration.
## [1.0.0] - 2026-04-27
### Added
- Added the initial `SKILL.md` with ZeroToken working discipline for token-efficient task execution.
- Added `agents/openai.yaml` with a host-facing ZeroToken prompt preset.
- Added minimal publishing files: `package.json`, `README.md`, and `CHANGELOG.md`.
FILE:package.json
{
"name": "zerotoken-skill",
"version": "1.0.3",
"files": [
"SKILL.md",
"README.md",
"CHANGELOG.md",
"agents/"
],
"description": "Default token-efficient assistant discipline for solving tasks with minimal precise prompts, concise context, and short actionable outputs.",
"author": "phoenixlucky",
"license": "MIT",
"keywords": [
"zerotoken",
"token-efficient",
"token saving",
"token budget",
"context optimization",
"context compression",
"context engineering",
"prompt engineering",
"prompt optimization",
"precise prompts",
"minimal prompts",
"prompt compression",
"agent discipline",
"agent workflow",
"default skill",
"progressive disclosure",
"context budget",
"concise",
"concise output",
"short answers",
"task completion",
"cost optimization",
"cost control",
"skill"
]
}
FILE:README.md
# ZeroToken Skill
Version: 1.0.3
License: MIT
`ZeroToken Skill` 是一个默认约束 Agent 以**最少必要 token**和**最精准提示词**完成任务的技能。
它不追求“看起来很短”,而是追求:
- 少读无关上下文
- 少做无效工具调用
- 少写重复解释
- 少输出无行动价值内容
- 用最短有效提示词提炼目标、输入、约束和输出格式
- 在准确性不下降的前提下压缩过程成本
## What It Does
这个 Skill 默认适用于多数任务,除非用户明确要求详尽解释、教学式展开、头脑风暴或广泛探索。它也特别适合以下场景:
- 默认希望输出简洁、直接、可执行
- 用户明确要求省 token / 简洁输出
- 需要降低上下文消耗和提示词成本
- 需要把复杂、含糊或冗长的问题改写成最少最精准的提示词
- 大资料总结,但只保留结论、证据、阻塞点、待办
- 小型编码 / 局部修复 / 配置调整,希望最小读取、最小改动、最小验证
- 需要 Progressive Disclosure:先定位,再局部读取,再完成任务
## Core Behavior
默认行为:
1. 先判断任务类型
2. 再分配上下文预算
3. 提炼最短有效提示词
4. 先给结果,再补关键依据
5. 不复述、不铺垫、不空转
## Task Modes
### A. 简单问答
- 直接回答
- 默认 1-5 句话
- 不主动扩展背景
### B. 代码小改
- 先定位,再局部读取
- 只改必要部分
- 只跑最小验证
### C. 多文件任务
- 先给短计划
- 每步只加载当前需要的文件
- 用事实清单代替长叙述
### D. 大资料总结
- 先识别输出目标
- 保留结论、数字、日期、负责人、阻塞点
- 用要点和证据位置代替大段复述
## Output Style
推荐短格式:
```text
已完成:...
改动:...
验证:...
注意:...
```
研究类任务推荐:
```text
结论:...
依据:...
不确定:...
下一步:...
```
## Safety and Quality Floor
ZeroToken 不是“偷 token”。
它仍然要求:
- 不牺牲准确性
- 不跳过必要验证
- 不把猜测写成事实
- 不用简短掩盖不确定性
一句话:
> 用最少必要 token,做出仍然可靠、能执行、可验证的结果。
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"
}
Diagnose ECS instance reboot or crash issues. First checks for abnormal maintenance events, then uses Cloud Assistant to check for internal restarts or kerne...
--- name: alibabacloud-ecs-reboot-or-crash-diagnosis description: Diagnose ECS instance reboot or crash issues. First checks for abnormal maintenance events, then uses Cloud Assistant to check for internal restarts or kernel panics. Use this skill when users report ECS instance unexpected reboot, crash, abnormal shutdown, kernel panic, or OOM. Supports vmcore file analysis, kdump configuration, system log analysis, and Windows crash dump analysis. metadata: pattern: pipeline steps: "5" required_params: "instance_id, region_id" domain: aiops owner: ecs-team contact: [email protected] ai-mode: disabled --- # ECS Instance Reboot/Crash Diagnosis Diagnose root cause of ECS instance unexpected reboot or crash. Uses standard workflow: check platform maintenance events first, then check internal system logs. Supports both Linux and Windows systems. ## Required Parameters Before starting diagnosis, **must** obtain the following parameters from user: | Parameter | Description | Example | |------|------|------| | `INSTANCE_ID` | ECS instance ID | `i-bp1a2b3c4d5e6f7g8h9j` | | `REGION_ID` | Region ID | `cn-hangzhou` | **If user does not provide any of the above parameters, must ask user first. Do not start diagnosis.** ## Mandatory Execution Rules 1. **Must obtain parameters first** — Instance ID and Region ID are required. Must ask user if missing. 2. **Standard workflow cannot be skipped** — Must execute in order: Maintenance Event Check → OSType Detection → System Log Check 3. **Must check Cloud Assistant status before diagnostics** — Before executing Step 3A/3B, must verify Cloud Assistant is running via `DescribeCloudAssistantStatus`. If not running, provide alternative diagnostic approaches. 4. **All diagnostic conclusions must be based on actual data** — No fabrication, speculation, or assumptions 5. **Output format must be strictly followed** — After diagnosis, **must read the complete template in `references/output-format.md`**, output strictly according to template structure. No free-form output, no omitted sections, no changed hierarchy. Every placeholder `{...}` in the template must be filled with actual data. --- ## Prerequisites ### CLI Tools - **aliyun-cli 3.3.3+** (required) — For calling Alibaba Cloud API - Installation & configuration: see [CLI Installation Guide](../../cli-installation-guide.md) ### AI-Mode Configuration (Required) Before using aliyun CLI commands, must configure AI-Mode: ```bash # Enable AI-Mode aliyun configure ai-mode enable # Set user-agent for skill identification aliyun configure ai-mode set-user-agent --user-agent "AlibabaCloud-Agent-Skills/alibabacloud-ecs-reboot-or-crash-diagnosis" # Update plugins aliyun plugin update ``` **After diagnosis complete, disable AI-Mode:** ```bash aliyun configure ai-mode disable ``` ### Alibaba Cloud Credentials Credentials must be pre-configured **outside of agent session**. Agent only verifies: ```bash aliyun configure list ``` ### Instance Requirements - **Cloud Assistant client must be installed and running** on the instance - Alibaba Cloud Linux: Pre-installed by default - Ubuntu/CentOS/Other: May require manual installation, check with `DescribeCloudAssistantStatus` API - Installation guide: https://help.aliyun.com/document_detail/64930.html - Instance status must be Running - **Note:** If Cloud Assistant is not running, diagnostic commands cannot be executed remotely. Must provide manual diagnostic steps to user. --- ## Required RAM Permissions See **[RAM Policies](references/ram-policies.md)** for the complete permission list and custom policy example. --- ## Step 1: Confirm Instance Information (Cannot Skip) **Verify instance exists and get basic information:** ```bash aliyun ecs describe-instances \ --biz-region-id <REGION_ID> \ --region <REGION_ID> \ --instance-ids '["<INSTANCE_ID>"]' ``` Confirm from returned JSON: - `RegionId` — Region ID (matches user provided) - `Status` — Instance status (Running/Stopped) - `InstanceName` — Instance name - `OSType` — Operating system type (**windows / linux**) **Record OSType for Step 3 branch selection.** --- ## Step 2: Check ECS Maintenance Events **Query instance historical system events to determine if platform maintenance caused reboot:** ```bash aliyun ecs describe-instance-history-events \ --biz-region-id <REGION_ID> \ --region <REGION_ID> \ --instance-id <INSTANCE_ID> \ --event-cycle-status Executed ``` **Event Analysis:** | Event Type | Meaning | Determination | Next Step | |---|---|---|---| | `SystemMaintenance.Reboot` | Reboot caused by system maintenance | Platform-initiated maintenance | Inform user, no further investigation needed | | `SystemFailure.Reboot` | Reboot caused by underlying hardware/system failure | Platform infrastructure failure | Suggest instance migration or contact support | | `InstanceFailure.Reboot` | Reboot caused by instance-level failure | **Instance internal issue detected by platform** | **Must continue to Step 3 for system log check** | | `InstanceExpiration.Stop` | Instance stopped due to expiration | Billing issue | Need renewal, no further investigation | | No relevant events | No platform maintenance events found | Not platform-initiated | Continue to Step 3 | **Important Notes for InstanceFailure.Reboot:** - This event indicates the platform detected an instance-level anomaly and triggered automatic recovery - Common causes: kernel panic, OOM, system hang, critical process failure - **Must execute Step 3** to check system logs for root cause - Even if no obvious errors in logs, the instance may have been unresponsive at kernel level **If maintenance event found:** - Clearly inform user of reboot cause (event type, time, reason) - Provide handling suggestions - End diagnosis flow **If no maintenance event found:** - Continue to Step 3, check internal system logs based on OSType --- ## Step 3A: Linux System Diagnosis (Execute when OSType is linux) ### Step 3A.1: Check Cloud Assistant Status (Mandatory) **Before executing diagnostic commands, verify Cloud Assistant is running:** ```bash aliyun ecs describe-cloud-assistant-status \ --biz-region-id <REGION_ID> \ --region <REGION_ID> \ --instance-id <INSTANCE_ID> ``` **Check the response:** ```json { "InstanceCloudAssistantStatusSet": { "InstanceCloudAssistantStatus": [ { "InstanceId": "i-xxx", "RegionId": "cn-xxx", "CloudAssistantStatus": "true", "LastHeartbeatTime": "2026-04-09T07:26:58Z" } ] } } ``` **Important Notes:** - `CloudAssistantStatus` is a **string** ("true"/"false"), not boolean - Check `LastHeartbeatTime` to ensure it's recent (within last few minutes) - Even if status is "true", RunCommand may still fail if service is unstable - **Always check RunCommand execution result** and handle failures gracefully - **Ubuntu vs RHEL differences:** - RHEL/CentOS/Alibaba Cloud Linux: Service name is `kdump`, crash files named `vmcore-*` - Ubuntu/Debian: Service name is `kdump-tools`, crash files named `dump.*` and `dmesg.*` - Diagnostic script now checks both service names and all crash file types **If CloudAssistantStatus is false or command fails:** - Cloud Assistant is not installed or not running on the instance - **Cannot proceed with remote diagnostic commands** - **Alternative approaches:** 1. Guide user to SSH into the instance and check logs manually 2. Provide manual diagnostic commands for user to execute 3. Suggest installing Cloud Assistant: [Installation Guide](https://help.aliyun.com/document_detail/64930.html) 4. Check instance monitoring data via CloudMonitor API **If CloudAssistantStatus is true:** - Proceed to Step 3A.2 ### Step 3A.2: Execute Linux Diagnostic Script Execute Linux diagnostic script via Cloud Assistant to check: - System reboot records (`last reboot`, `/var/log/messages` or `/var/log/syslog`) - Kernel Panic records (`dmesg`) - OOM records and `vm.panic_on_oom` configuration - Kdump configuration and crash dump file status - **Crash dump files**: vmcore (RHEL/CentOS) or dump.*/dmesg.* (Ubuntu/Debian) **Complete diagnostic commands: see [diagnostic-commands.md](references/diagnostic-commands.md#linux-system-diagnosis)** **Linux Result Analysis:** | Finding | Possible Cause | Suggestion | |---|---|---| | Kernel Panic + crash dump (vmcore/dump.*) | Kernel crash, dump file generated | Read dmesg.* file for panic reason, contact Alibaba Cloud technical support for deep analysis | | Kernel Panic + no crash dump | Kernel crash, but kdump not configured or not working | **Proceed to Step 5**: Recommend Kdump configuration for future crash capture | | OOM + panic_on_oom=1 | OOM triggered kernel panic | Disable panic_on_oom or increase memory | | OOM Killer | Memory insufficient causing process killed | Optimize memory usage or upgrade instance type | | SysRq triggered crash | Manual crash trigger via `/proc/sysrq-trigger` | Check if intentional test, review bash history and audit logs | | Normal reboot records | User or program triggered reboot | Check cron jobs or ops scripts | | No abnormal records | No system-level issues found | May be external factors, suggest monitoring | --- ## Step 3B: Windows System Diagnosis (Execute when OSType is windows) ### Step 3B.1: Check Cloud Assistant Status (Mandatory) **Before executing diagnostic commands, verify Cloud Assistant is running:** ```bash aliyun ecs describe-cloud-assistant-status \ --biz-region-id <REGION_ID> \ --region <REGION_ID> \ --instance-id <INSTANCE_ID> ``` **Check the response:** - `CloudAssistantStatus: true` — Cloud Assistant is running, proceed to Step 3B.2 - `CloudAssistantStatus: false` — Cloud Assistant is not running - **Cannot proceed with remote diagnostic commands** - Guide user to SSH/RDP into instance and run diagnostics manually - Suggest reinstalling Cloud Assistant: [Windows Installation Guide](https://help.aliyun.com/document_detail/64930.html) ### Step 3B.2: Execute Windows Diagnostic Script Execute Windows diagnostic script via Cloud Assistant to check: - System uptime and unexpected shutdown events (Event ID 41, 1074, 6008, 6006) - Memory dump configuration and pagefile settings - MEMORY.DMP and minidump files existence - BSOD events and application crashes **Complete diagnostic commands: see [diagnostic-commands.md](references/diagnostic-commands.md#windows-system-diagnosis)** **Windows Result Analysis:** | Finding | Possible Cause | Suggestion | |---|---|---| | Event 41 (Kernel-Power) | Unexpected shutdown/crash | Check for BSOD, dump files | | Dump configured + dump file exists | System crashed and captured dump | Contact Alibaba Cloud technical support for dump file analysis | | Dump configured + no dump file | Crash occurred but no dump captured | Check pagefile and disk space | | Dump not configured | Crash dumps disabled | Enable memory dump for diagnosis | | BSOD events found | Blue screen crash occurred | Check bug check code in dump | | No abnormal events | No system-level crash records | May be power issue or external factor | --- ## Step 3.5: Get Cloud Assistant Command Output (Required after Step 3) After executing diagnostic script via `RunCommand`, query the execution result: ```bash aliyun ecs describe-invocations \ --biz-region-id <REGION_ID> \ --region <REGION_ID> \ --instance-id <INSTANCE_ID> \ --invoke-id <INVOKE_ID> ``` **Important Notes:** - Use `--instance-id` (not `--instance-id.1`) for describe-invocations API - The `InvokeId` is returned by the `RunCommand` API call - Decode the `Output` field from Base64 to get diagnostic results - Check `InvokeStatus` to ensure command execution completed successfully --- ## Step 4: Analyze Crash Dump Files If Step 3 found crash dump files (vmcore on Linux, MEMORY.DMP/minidump on Windows), perform preliminary analysis. **Complete analysis commands: see [diagnostic-commands.md](references/diagnostic-commands.md#crash-dump-analysis)** > **Important:** If Linux vmcore files need deep analysis or Windows dump files (MEMORY.DMP/minidump) are found, recommend the user contact Alibaba Cloud technical support team for professional crash dump analysis assistance. --- ## Step 5: Recommend Kdump Configuration (If Not Configured) **If Step 3A found Kernel Panic records but no vmcore files, must advise user to configure Kdump.** ### When to Recommend Kdump Configuration - Kernel panic records found in dmesg or system logs, but `/var/crash` has no vmcore files - Kdump service status shows `inactive` or `failed` - `/proc/cmdline` does not contain `crashkernel=` parameter ### Key Points to Communicate 1. **Why Kdump is needed**: Without Kdump, kernel crashes will not generate vmcore files, making root cause analysis impossible. 2. **Configuration requirements**: - Reserve memory for crash kernel via `crashkernel=` kernel parameter - Enable and start the kdump (RHEL/CentOS) or kdump-tools (Ubuntu/Debian) service - Ensure sufficient disk space in `/var/crash` (or configured path) 3. **Configuration reference**: Provide guidance from [diagnostic-commands.md](references/diagnostic-commands.md#kdump-配置建议) ### Kdump Configuration Steps Summary **RHEL/CentOS/Alibaba Cloud Linux:** 1. Install: `yum install -y kexec-tools` 2. Add `crashkernel=auto` to kernel parameters in `/etc/default/grub` 3. Run `grub2-mkconfig -o /boot/grub2/grub.cfg` 4. Reboot the instance 5. Enable: `systemctl enable --now kdump` **Ubuntu/Debian:** 1. Install: `apt-get install -y kdump-tools` 2. Set `USE_KDUMP=1` in `/etc/default/kdump-tools` 3. Run `update-grub` (crashkernel parameter usually auto-added) 4. Reboot the instance 5. Verify: `systemctl status kdump-tools` ### Windows Memory Dump Configuration If Step 3B found BSOD events but no dump files: 1. Verify pagefile is configured and has sufficient size 2. Enable memory dump: System Properties → Advanced → Startup and Recovery → Settings 3. Select "Automatic memory dump" or "Kernel memory dump" 4. Ensure `CrashDumpEnabled` registry value is not 0 --- ## Final Output (Must execute after diagnosis complete) **After all diagnostic steps complete, must do both of the following:** 1. **Read `references/output-format.md`** — Get complete output format template 2. **Output strictly according to template structure** — Choose corresponding template based on actual result --- ## References - **[Output Format](references/output-format.md)** — Diagnostic result output template - **[Common Scenarios](references/scenarios.md)** — Typical problem diagnosis examples - **[Diagnostic Commands](references/diagnostic-commands.md)** — Complete diagnostic scripts and analysis commands FILE:references/diagnostic-commands.md # Diagnostic Commands Reference 本文档提供诊断检查项和命令参考。**Agent 应根据实际操作系统类型和版本,动态生成适配的诊断命令。** > **重要原则**: > - 不同 Linux 发行版的服务名称、日志路径、工具命令可能不同 > - 先通过 `DescribeInstances` 获取 OSType 和 OSName,再生成适配的命令 > - 命令应包含错误处理,避免因路径不存在或命令不可用而中断 --- ## Linux 系统诊断 ### 检查项清单 | 检查项 | 目的 | 参考命令 | |--------|------|----------| | 系统重启记录 | 查看历史重启时间和来源 | `last reboot`, `who -b` | | 系统日志中的重启/关机记录 | 识别正常/异常关机 | `grep -i "reboot\|shutdown" /var/log/messages` 或 `/var/log/syslog` | | Kernel Panic 记录 | 检测内核崩溃 | `dmesg | grep -i panic`, 日志文件搜索 | | OOM Killer 记录 | 检测内存不足导致的进程终止 | 日志文件搜索 "Out of memory", "oom", "Kill process" | | OOM Panic 配置 | 判断 OOM 是否会触发系统重启 | `sysctl -n vm.panic_on_oom` | | Kdump 服务状态 | 验证崩溃转储是否配置并启用 | `systemctl status kdump` 或 `systemctl status kdump-tools` | | Kdump 配置文件 | 获取转储文件存储路径 | `/etc/kdump.conf` 或 `/etc/default/kdump-tools` | | 崩溃转储文件 | 检查是否存在 vmcore/dump 文件 | 检查配置的路径或默认 `/var/crash` | ### 操作系统差异对照表 | 项目 | RHEL/CentOS/Alibaba Cloud Linux | Ubuntu/Debian | |------|--------------------------------|---------------| | 系统日志路径 | `/var/log/messages` | `/var/log/syslog` | | Kdump 服务名 | `kdump` | `kdump-tools` | | Kdump 配置文件 | `/etc/kdump.conf` | `/etc/default/kdump-tools` | | 崩溃转储文件名 | `vmcore` (目录: `127.0.0.1-date-time/`) | `dump.*` + `dmesg.*` | | 默认转储路径 | `/var/crash` | `/var/crash` | ### 命令示例参考 以下命令仅供参考,Agent 应根据实际操作系统动态调整。 #### 1. 获取系统信息 ```bash # 操作系统版本 cat /etc/os-release # 内核版本 uname -r # 系统运行时间 uptime ``` #### 2. 系统重启历史 ```bash # 重启历史记录 last reboot | head -10 # 最近一次启动时间 who -b ``` #### 3. 系统日志检查 **RHEL/CentOS/Alibaba Cloud Linux:** ```bash # 重启/关机相关日志 grep -i "reboot\|shutdown\|restart" /var/log/messages | tail -20 # Kernel Panic 记录 dmesg | grep -i "panic\|oops" | tail -20 grep -i "kernel panic" /var/log/messages | tail -10 # OOM 记录 grep -i "out of memory\|oom\|kill process" /var/log/messages | tail -20 ``` **Ubuntu/Debian:** ```bash # 重启/关机相关日志 grep -i "reboot\|shutdown\|restart" /var/log/syslog | tail -20 # Kernel Panic 记录 dmesg | grep -i "panic\|oops" | tail -20 grep -i "kernel panic" /var/log/syslog | tail -10 # OOM 记录 grep -i "out of memory\|oom\|kill process" /var/log/syslog | tail -20 ``` #### 4. OOM Panic 配置 ```bash # 检查 OOM 时是否触发 panic sysctl -n vm.panic_on_oom # 返回值说明: # 0 - OOM 只杀死进程,系统继续运行 # 1 - OOM 触发 kernel panic,导致系统重启 ``` #### 5. Kdump 状态检查 **RHEL/CentOS/Alibaba Cloud Linux:** ```bash # 检查 kdump 服务状态 systemctl status kdump # 检查是否已配置 cat /etc/kdump.conf # 获取转储路径 (默认 /var/crash) grep "^path" /etc/kdump.conf ``` **Ubuntu/Debian:** ```bash # 检查 kdump-tools 服务状态 systemctl status kdump-tools # 检查是否已配置 cat /etc/default/kdump-tools # 检查内核 crashkernel 参数 cat /proc/cmdline | grep crashkernel ``` #### 6. 崩溃转储文件检查 ```bash # 检查转储目录 ls -la /var/crash/ # RHEL/CentOS: 查找 vmcore 文件 find /var/crash -name "vmcore*" -type f -exec ls -lh {} \; # Ubuntu/Debian: 查找 dump 和 dmesg 文件 find /var/crash -name "dump.*" -type f -exec ls -lh {} \; find /var/crash -name "dmesg.*" -type f -exec ls -lh {} \; # 检查最近 7 天的转储文件 find /var/crash -type f \( -name "vmcore*" -o -name "dump.*" -o -name "dmesg.*" \) -mtime -7 ``` --- ## Kdump 配置建议 ### 何时需要建议配置 Kdump 以下情况应建议用户配置 Kdump: 1. **检测到 Kernel Panic 迹象但无 vmcore 文件** - dmesg 或系统日志中有 panic 记录 - 但 `/var/crash` 目录为空或不存在 2. **Kdump 服务未运行** - `systemctl status kdump` 显示 inactive/failed - `systemctl status kdump-tools` 显示 inactive/failed 3. **内核未配置 crashkernel 参数** - `/proc/cmdline` 中没有 `crashkernel=` 参数 ### Kdump 配置参考 **RHEL/CentOS/Alibaba Cloud Linux:** ```bash # 1. 安装 kexec-tools (如未安装) yum install -y kexec-tools # 2. 配置 /etc/kdump.conf # 默认配置通常可用,关键配置项: # path /var/crash # 转储文件存储路径 # core_collector makedumpfile -l --message-level 1 -d 31 # 压缩转储 # 3. 在 /etc/default/grub 的 GRUB_CMDLINE_LINUX 中添加 crashkernel 参数 # crashkernel=auto 或 crashkernel=128M # 4. 更新 grub 配置 grub2-mkconfig -o /boot/grub2/grub.cfg # 5. 重启系统使 crashkernel 生效 reboot # 6. 启用并启动 kdump 服务 systemctl enable kdump systemctl start kdump systemctl status kdump ``` **Ubuntu/Debian:** ```bash # 1. 安装 kdump-tools apt-get install -y kdump-tools # 2. 配置 /etc/default/kdump-tools # USE_KDUMP=1 # 3. 更新 grub 配置 (安装时通常会自动添加 crashkernel 参数) update-grub # 4. 重启系统 reboot # 5. 验证服务状态 systemctl status kdump-tools ``` ### Kdump 配置验证 ```bash # 验证 crashkernel 参数已生效 cat /proc/cmdline | grep crashkernel # 验证 kdump 服务状态 systemctl status kdump # RHEL/CentOS systemctl status kdump-tools # Ubuntu/Debian ``` --- ## Windows 系统诊断 ### 检查项清单 | 检查项 | 目的 | 参考 PowerShell 命令 | |--------|------|----------------------| | 系统信息 | 获取 Windows 版本和主机名 | `Get-ComputerInfo` | | 系统运行时间 | 判断最近是否重启过 | `[WMI]'\\.\root\cimv2:Win32_OperatingSystem'` | | 意外关机事件 | 检测非正常关机 | Event ID 41, 6008, 6006, 1074 | | 内存转储配置 | 验证是否配置了崩溃转储 | 注册表 `CrashControl` | | 页面文件配置 | 转储文件需要页面文件支持 | `Get-CimInstance Win32_PageFileUsage` | | MEMORY.DMP 文件 | 检查完整内存转储文件 | `Test-Path C:\Windows\MEMORY.DMP` | | Minidump 文件 | 检查小型转储文件 | `Get-ChildItem C:\Windows\Minidump` | | BSOD 事件 | 检测蓝屏错误报告 | WER 事件日志 | ### 事件 ID 说明 | Event ID | 来源 | 含义 | |----------|------|------| | 41 | Kernel-Power | 系统意外重启(未正常关机) | | 1074 | User32 | 正常关机/重启,记录原因 | | 6008 | EventLog | 上次关机是意外的 | | 6006 | EventLog | 事件日志服务已停止(正常关机) | ### 内存转储类型 | CrashDumpEnabled | 类型 | 说明 | |------------------|------|------| | 0 | None | 禁用内存转储 | | 1 | Complete | 完整内存转储(最大,约等于内存大小) | | 2 | Kernel | 内核内存转储(中等大小) | | 3 | Small | 小内存转储(64KB,Minidump) | | 7 | Automatic | 自动内存转储(推荐) | ### PowerShell 命令示例 ```powershell # 系统信息 Get-ComputerInfo | Select-Object WindowsProductName, WindowsVersion, OsArchitecture, CsName # 系统运行时间 $os = Get-CimInstance Win32_OperatingSystem $uptime = (Get-Date) - $os.LastBootUpTime Write-Host "Last boot: $($os.LastBootUpTime)" Write-Host "Uptime: $($uptime.Days) days, $($uptime.Hours) hours" # 意外关机事件 Get-WinEvent -FilterHashtable @{LogName="System"; ID=41,1074,6008,6006} -ErrorAction SilentlyContinue | Select-Object TimeCreated, Id, Message -First 10 # 内存转储配置 $crashControl = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\CrashControl" Write-Host "CrashDumpEnabled: $($crashControl.CrashDumpEnabled)" Write-Host "DumpFile: $($crashControl.DumpFile)" Write-Host "MinidumpDir: $($crashControl.MinidumpDir)" # 页面文件配置 Get-CimInstance Win32_PageFileUsage | Select-Object Name, AllocatedBaseSize, CurrentUsage # 检查 MEMORY.DMP $dumpFile = $crashControl.DumpFile if (-not $dumpFile) { $dumpFile = "C:\Windows\MEMORY.DMP" } if (Test-Path $dumpFile) { $fileInfo = Get-Item $dumpFile Write-Host "MEMORY.DMP found: Size=$([math]::Round($fileInfo.Length/1GB,2)) GB, Modified=$($fileInfo.LastWriteTime)" } # 检查 Minidump 文件 $minidumpDir = $crashControl.MinidumpDir if (-not $minidumpDir) { $minidumpDir = "C:\Windows\Minidump" } if (Test-Path $minidumpDir) { Get-ChildItem -Path $minidumpDir -Filter "*.dmp" | Sort-Object LastWriteTime -Descending | Select-Object -First 5 } # BSOD 事件 Get-WinEvent -FilterHashtable @{LogName="System"; ProviderName="Microsoft-Windows-WER-SystemErrorReporting"} -ErrorAction SilentlyContinue | Select-Object TimeCreated, Id, Message -First 10 ``` ### Windows 内存转储配置建议 当检测到 BSOD 事件但无转储文件时,建议配置内存转储: 1. **通过系统属性配置**: - 右键"此电脑" → 属性 → 高级系统设置 - 启动和故障恢复 → 设置 - 选择"自动内存转储"或"内核内存转储" - 确保页面文件大小足够(至少内存大小 + 1MB) 2. **PowerShell 配置**: ```powershell # 设置自动内存转储 Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\CrashControl" -Name "CrashDumpEnabled" -Value 7 # 确保页面文件存在且大小足够 # 通常由系统自动管理,检查方法: $cs = Get-CimInstance Win32_ComputerSystem if ($cs.AutomaticManagedPagefile) { Write-Host "Pagefile is automatically managed" } else { # 手动配置页面文件 # 需要重启生效 } ``` --- ## 崩溃转储文件分析 ### Linux vmcore 分析 如果找到 vmcore 文件,可读取 `vmcore-dmesg.txt` 进行初步分析: ```bash # 查看 vmcore-dmesg.txt 内容 cat /var/crash/127.0.0.1-*/vmcore-dmesg.txt # 关键信息搜索 grep -i "kernel panic" /var/crash/*/vmcore-dmesg.txt grep -i "RIP:" /var/crash/*/vmcore-dmesg.txt grep -i "Call Trace" /var/crash/*/vmcore-dmesg.txt ``` **关键信息解读**: | 关键字 | 含义 | |--------|------| | `Kernel panic - not syncing: VFS` | 文件系统相关问题 | | `Kernel panic - not syncing: Attempted to kill init` | init 进程崩溃 | | `Kernel panic - not syncing: Out of memory` | OOM 导致崩溃 | | `RIP: 0010:` | 崩溃时的指令位置 | | `Call Trace:` | 调用栈 | | `MCE` / `Machine Check Exception` | 硬件错误 | > **注意**:深度 vmcore 分析需要使用 `crash` 工具和调试符号包,建议联系阿里云技术支持获取专业分析。 ### Windows 转储文件分析 使用 WinDbg 或 BlueScreenView 工具分析: ``` # WinDbg 命令 !analyze -v # 自动分析崩溃原因 k # 查看调用栈 .bugcheck # 查看 bugcheck 代码 ``` > **注意**:深度 dump 分析建议联系阿里云技术支持。 --- ## 通过云助手执行命令 诊断命令通过阿里云云助手远程执行: ### Linux 命令执行 ```bash aliyun ecs run-command \ --biz-region-id <REGION_ID> \ --region <REGION_ID> \ --type RunShellScript \ --instance-id <INSTANCE_ID> \ --timeout 3600 \ --command-content '<SCRIPT_CONTENT>' ``` ### Windows 命令执行 ```bash aliyun ecs run-command \ --biz-region-id <REGION_ID> \ --region <REGION_ID> \ --type RunPowerShellScript \ --instance-id <INSTANCE_ID> \ --timeout 3600 \ --command-content '<SCRIPT_CONTENT>' ``` ### 获取命令执行结果 ```bash aliyun ecs describe-invocations \ --biz-region-id <REGION_ID> \ --region <REGION_ID> \ --instance-id <INSTANCE_ID> \ --invoke-id <INVOKE_ID> ``` **注意**:`Output` 字段为 Base64 编码,需要解码后查看。 FILE:references/output-format.md # Output Format Requirements After diagnosis is complete, output results according to the following structure. ## Table of Contents 1. [Linux Diagnosis Result](#linux-diagnosis-result) 2. [Windows Diagnosis Result](#windows-diagnosis-result) --- ## Linux Diagnosis Result ```markdown ## Diagnostic Progress ### Step 1: Confirm Instance Information > First need to confirm instance basic info and region. - Instance ID: {instance_id} - Region: {region_id} - OS Type: Linux - Current Status: {status} ### Step 2: Check Maintenance Events > Check if platform maintenance events caused reboot. **Findings:** - {event_query_result} ### Step 3A: Linux System Diagnosis (if needed) > No maintenance events found, checking internal restart or panic records. **Cloud Assistant Status Check:** - Cloud Assistant Running: {yes/no} - If no: {explain why cannot proceed and provide alternative approaches} **Findings:** - {cloud_assistant_execution_result} **Kdump Configuration Status:** - Service Status: {active/inactive/active (kdump-tools)} - Service Type: {kdump (RHEL/CentOS) / kdump-tools (Ubuntu/Debian)} - Crash Dump Path: {configured_path} **OOM Panic Configuration:** - vm.panic_on_oom: {0/1} - Impact: {OOM kills process only / OOM triggers kernel panic and reboot} **Crash Dump File Check:** - Found crash dumps: {yes/no} - Dump type: {vmcore (RHEL) / dump.*+dmesg.* (Ubuntu)} - Latest dump: {file_path, size, time} - Panic reason (from dmesg): {panic_message_if_available} **Alternative Diagnostic Approaches (if Cloud Assistant not available):** ```bash # Provide these commands to user for manual execution via SSH ssh root@{instance_public_ip} # Check reboot history last reboot # Check system logs grep -i "reboot\|shutdown\|panic\|oom" /var/log/syslog | tail -50 # Check dmesg for errors dmesg | grep -i "panic\|oom\|error" | tail -20 # Check kdump systemctl status kdump ls -lh /var/crash/ ``` ### Step 4A: vmcore-dmesg.txt Analysis (if vmcore found) > Found vmcore file, reading vmcore-dmesg.txt for preliminary analysis. **Panic Reason:** - {panic_specific_reason} **Crash Location:** - RIP: {function_and_address_at_crash} - Involved Modules: {related_kernel_modules} **Call Stack:** ``` {key_call_stack_fragment} ``` ### Step 5: Kdump Configuration Recommendation (if no vmcore and kdump not configured) > Kernel panic detected but no crash dump file found. Kdump is not properly configured. **Current Kdump Status:** - Service Status: {kdump_service_status} - crashkernel Parameter: {present/absent in /proc/cmdline} - Config File Exists: {yes/no} **Why Kdump is Needed:** Without Kdump configured, kernel crashes will not generate vmcore files, making root cause analysis impossible for future occurrences. **Configuration Steps for {OS_Type}:** {configuration_steps_based_on_os} --- ## Diagnostic Conclusion - **Root Cause Analysis**: {root_cause} - **Impact Scope**: {impact_scope} --- ## Recommendations 1. {recommendation_1} 2. {recommendation_2} ``` --- ## Windows Diagnosis Result ```markdown ## Diagnostic Progress ### Step 1: Confirm Instance Information > First need to confirm instance basic info and region. - Instance ID: {instance_id} - Region: {region_id} - OS Type: Windows - Current Status: {status} ### Step 2: Check Maintenance Events > Check if platform maintenance events caused reboot. **Findings:** - {event_query_result} ### Step 3B: Windows System Diagnosis (if needed) > No maintenance events found, checking Windows crash dump and event logs. **System Uptime:** - Last Boot Time: {last_boot_time} - Uptime: {days} days, {hours} hours **Unexpected Shutdown Events:** - {shutdown_event_summary} **Memory Dump Configuration:** - CrashDumpEnabled: {0/1/2/3/7} - Dump Type: {None/Complete/Kernel/Small/Automatic} - Dump File Path: {dump_file_path} - Pagefile: {configured/not configured} **Memory Dump File Check:** - Memory dump file: {found/not found} - File size: {size} - Last modified: {timestamp} **Minidump Files:** - Count: {count} - Latest: {filename, timestamp} **BSOD Events:** - {bsod_event_summary} --- ## Diagnostic Conclusion - **Root Cause Analysis**: {root_cause} - **Impact Scope**: {impact_scope} --- ## Recommendations 1. {recommendation_1} 2. {recommendation_2} ``` FILE:references/ram-policies.md # RAM 权限清单 本 Skill 执行所需的 RAM 权限(最小权限原则): ## 必需权限 `ecs:DescribeInstances` — 确认实例存在并获取基本信息(状态、名称、操作系统类型) `ecs:DescribeInstanceAttribute` — 获取实例详细属性,用于操作系统类型检测和分支选择 `ecs:DescribeInstanceHistoryEvents` — 查询实例历史维护事件,判断是否为平台触发重启 `ecs:DescribeCloudAssistantStatus` — 验证云助手运行状态,确保远程诊断命令可执行 `ecs:RunCommand` — 通过云助手执行诊断脚本(Linux Shell 或 Windows PowerShell) `ecs:DescribeInvocations` — 获取云助手命令执行结果,提取诊断输出 ## 权限说明 - **权限范围**: 仅包含诊断所需的只读和命令执行权限 - **写操作**: 无(本 Skill 不修改实例配置) - **通配符**: 未使用(遵循最小权限原则) ## 自定义策略示例 ```json { "Version": "1", "Statement": [ { "Effect": "Allow", "Action": [ "ecs:DescribeInstances", "ecs:DescribeInstanceAttribute", "ecs:DescribeInstanceHistoryEvents", "ecs:DescribeCloudAssistantStatus", "ecs:RunCommand", "ecs:DescribeInvocations" ], "Resource": "*" } ] } ``` ## 使用场景 此权限配置适用于 ECS 实例故障诊断场景: - 检查平台维护事件 - 通过云助手远程执行诊断命令 - 获取系统日志和崩溃转储文件信息 - 分析重启/崩溃根因 FILE:references/scenarios.md # Common Diagnostic Scenarios This document lists common diagnostic scenarios and expected outputs for the ecs-reboot-or-crash skill. --- ## Linux Scenarios ### Scenario 1: System Maintenance Reboot ``` Diagnosis Result: - Found event: SystemMaintenance.Reboot - Event time: 2025-03-20 10:00:00 - Reason: Planned system maintenance Conclusion: Instance reboot was caused by Alibaba Cloud platform maintenance, which is normal ops activity. ``` --- ### Scenario 2: Kernel Panic + vmcore Available ``` Diagnosis Result: - No maintenance events found - Cloud Assistant found: "Kernel panic - not syncing" record in dmesg - Kdump service status: active - Found vmcore: /var/crash/127.0.0.1-2025-03-20-10:30:00/vmcore (2.5G) - vmcore time: 2025-03-20 10:30:00 vmcore-dmesg.txt analysis: - Panic reason: Kernel panic - not syncing: Fatal exception in interrupt - Crash location: RIP: 0010:nvme_queue_rq+0x1a2/0x4d0 [nvme] - Involved module: nvme driver - Call stack: nvme_queue_rq -> blk_mq_dispatch_rq_list -> ... Conclusion: Instance rebooted due to NVMe driver abnormality causing kernel crash, vmcore dump file generated. Suggestion: Check NVMe driver version, upgrade driver or kernel if needed. Use crash tool for deeper vmcore analysis. ``` --- ### Scenario 3: Kernel Panic + No vmcore ``` Diagnosis Result: - No maintenance events found - Cloud Assistant found: "Kernel panic" record in dmesg - Kdump service status: inactive - Found vmcore: No Conclusion: Instance rebooted due to kernel crash, but kdump not configured or not working, unable to capture vmcore. Suggestion: Configure kdump service to capture vmcore on next crash for root cause analysis. ``` --- ### Scenario 4: OOM with panic_on_oom Enabled ``` Diagnosis Result: - No maintenance events found - Cloud Assistant found: "Out of memory: Kill process" record in /var/log/messages - vm.panic_on_oom: 1 (OOM triggers kernel panic) - Kdump service status: active - Found vmcore: Yes Conclusion: OOM event triggered kernel panic because vm.panic_on_oom=1, causing system reboot. Suggestions: 1. Disable panic_on_oom: sysctl -w vm.panic_on_oom=0 (add to /etc/sysctl.conf for persistence) 2. Optimize application memory usage or upgrade instance type 3. Review OOM killed processes to identify memory-hungry applications ``` --- ### Scenario 5: OOM Killer Only ``` Diagnosis Result: - No maintenance events found - Cloud Assistant found: "Out of memory: Kill process" record in /var/log/messages - vm.panic_on_oom: 0 (OOM only kills process, no panic) - No kernel panic records found Conclusion: Instance triggered OOM Killer due to insufficient memory, some processes were terminated but system continued running. Suggestion: Optimize application memory usage, or upgrade instance type. ``` --- ## Windows Scenarios ### Scenario 6: BSOD with Memory Dump ``` Diagnosis Result: - No maintenance events found - Unexpected shutdown event: Event ID 41 (Kernel-Power) - BSOD events found in WER logs - Memory dump configuration: Automatic memory dump (CrashDumpEnabled=7) - Memory dump file found: C:\Windows\MEMORY.DMP (4.2 GB) - Dump time: 2025-03-20 10:30:00 Conclusion: Windows BSOD crash occurred, memory dump captured. Suggestion: Download MEMORY.DMP and analyze with WinDbg: 1. Install Windows Debugging Tools 2. Open dump file in WinDbg 3. Run: !analyze -v ``` --- ### Scenario 7: BSOD without Dump (Not Configured) ``` Diagnosis Result: - No maintenance events found - Unexpected shutdown event: Event ID 41 (Kernel-Power) - BSOD events found in WER logs - Memory dump configuration: None (CrashDumpEnabled=0) - Memory dump file: Not found Conclusion: Windows BSOD crash occurred but memory dump was not configured. Suggestions: 1. Enable memory dump: System Properties > Advanced > Startup and Recovery > Settings 2. Select "Automatic memory dump" or "Kernel memory dump" 3. Ensure pagefile is configured and has sufficient space ``` --- ### Scenario 8: BSOD without Dump (Pagefile Issue) ``` Diagnosis Result: - No maintenance events found - Unexpected shutdown event: Event ID 41 (Kernel-Power) - Memory dump configuration: Automatic memory dump (CrashDumpEnabled=7) - Pagefile: Not configured - Memory dump file: Not found Conclusion: Windows BSOD crash occurred but memory dump was not captured because pagefile is not configured. Suggestions: 1. Configure pagefile: System Properties > Advanced > Performance > Settings > Advanced > Virtual memory 2. Set pagefile size to at least RAM size + 1MB 3. Reboot for pagefile changes to take effect ``` --- ### Scenario 9: Minidump Available ``` Diagnosis Result: - No maintenance events found - Unexpected shutdown event: Event ID 41 (Kernel-Power) - Memory dump configuration: Small memory dump (CrashDumpEnabled=3) - Minidump files found in C:\Windows\Minidump: - 032025-12345-01.dmp (128 KB, 2025-03-20 10:30:00) Conclusion: Windows crash occurred, minidump captured. Suggestion: Analyze minidump with WinDbg Preview (Microsoft Store) or BlueScreenView tool. ``` --- ### Scenario 10: Application Crash Causing Instability ``` Diagnosis Result: - No maintenance events found - No unexpected shutdown events - Application crash events found: Multiple crashes of {application_name}.exe - No system crash dump files Conclusion: Application crashes detected but no system-level crash. System remained running. Suggestions: 1. Check application logs for crash details 2. Verify application compatibility with Windows version 3. Check for application updates or known issues ```