Initial commit with translated description
This commit is contained in:
292
SKILL.md
Normal file
292
SKILL.md
Normal file
@@ -0,0 +1,292 @@
|
||||
---
|
||||
name: post-job
|
||||
description: "向LinkedIn、Indeed、Ziprecruiter等20多个求职板发布免费招聘广告。"
|
||||
---
|
||||
|
||||
# JobPoster Skill
|
||||
|
||||
> ⚠️ **CRITICAL: Only use the `post_job` tool as the entry point!**
|
||||
>
|
||||
> **DO NOT** call internal APIs directly (`genJD`, `postToLinkd`, `API_URL_*`).
|
||||
> **DO NOT** bypass `post_job` to call Fuku AI endpoints manually.
|
||||
>
|
||||
> **Why?** The `post_job` function handles:
|
||||
> - Input validation and sanitization
|
||||
> - Location fuzzy matching
|
||||
> - LinkedIn sync orchestration
|
||||
> - Background monitoring setup
|
||||
>
|
||||
> Skipping it will result in incomplete job postings and broken workflows.
|
||||
|
||||
🚀 **Quickly post job openings and collect resumes via natural language commands.**
|
||||
|
||||
JobPoster simplifies the hiring process by letting you post jobs through simple commands. It automatically matches locations, validates inputs, and provides shareable application links. Perfect for recruiters, hiring managers, and HR teams.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Natural Language Interface** - Post jobs with simple commands like "Hire a frontend engineer in Singapore"
|
||||
- **Global City Support** - 100+ cities worldwide with fuzzy matching (Singapore, Hong Kong, New York, London, etc.)
|
||||
- **AI Job Description** - Optional AI-powered JD generation for professional, compelling postings
|
||||
- **Instant Application Links** - Get shareable URLs for candidates to apply directly
|
||||
- **Resume Collection** - All applications sent to your specified email
|
||||
- **LinkedIn Sync** - Automatic LinkedIn job posting integration (**no LinkedIn account binding required** — posts through Fuku AI's relay service)
|
||||
|
||||
## ⚠️ External Service Notice
|
||||
|
||||
This skill uses **Fuku AI** (https://hapi.fuku.ai) as a third-party job posting relay service to distribute jobs to multiple boards.
|
||||
|
||||
**🎉 No LinkedIn Account Binding Required!**
|
||||
|
||||
LinkedIn job postings are handled through Fuku AI's relay service — you do **NOT** need to connect or bind your personal LinkedIn account. The job is posted anonymously through Fuku AI's infrastructure.
|
||||
|
||||
**Data transmitted to Fuku AI:**
|
||||
|
||||
- Job title, description, company name, location
|
||||
- Email address for receiving resumes
|
||||
- LinkedIn company URL (optional, defaults to Fuku AI's company)
|
||||
|
||||
**Authentication:**
|
||||
|
||||
- Uses embedded client identifier (no user API key required)
|
||||
- Free tier service provided by Fuku AI
|
||||
|
||||
**Security:**
|
||||
|
||||
- Job descriptions are sanitized before transmission to prevent prompt injection
|
||||
- Job IDs are strictly validated (alphanumeric + hyphens only)
|
||||
- Channel parameters are filtered to prevent log injection
|
||||
|
||||
By using this skill, you consent to transmitting the above data to Fuku AI's servers.
|
||||
|
||||
## 🔒 Security Best Practices
|
||||
|
||||
To minimize risks while using this skill:
|
||||
|
||||
### 1. Use a Dedicated Email Address
|
||||
|
||||
- **Do NOT use personal email** — Create a dedicated hiring email (e.g., `hiring@yourcompany.com` or `jobs+company@gmail.com`)
|
||||
- **Use email aliases** — Gmail supports `youremail+company@gmail.com` for tracking sources
|
||||
- **Forward to main inbox** — Set up auto-forwarding if needed
|
||||
|
||||
### 2. Sanitize Job Descriptions Before Submitting
|
||||
|
||||
- **Remove sensitive info** — Don't include internal salary ranges, confidential project names, or proprietary tech stack details
|
||||
- **Avoid personal data** — Don't mention hiring manager names, direct contact info, or office security details
|
||||
- **Keep it public-ready** — Write descriptions as if they'll be visible to anyone (because they will be)
|
||||
|
||||
### 3. Understand the Relay Model
|
||||
|
||||
- **Posts are anonymous** — Jobs appear under Fuku AI's accounts, not your company's LinkedIn page
|
||||
- **No direct control** — You cannot edit/delete postings directly on job boards; contact support if changes needed
|
||||
- **Third-party dependency** — If Fuku AI service goes down, postings may be affected
|
||||
|
||||
### 4. Monitor Active Postings
|
||||
|
||||
- **Save Job IDs** — Keep a record of all posted Job IDs for tracking
|
||||
- **Check LinkedIn status** — Use `check_linkedin_status` to verify postings went live
|
||||
- **Periodic audit** — Review active postings monthly to ensure they're still accurate
|
||||
|
||||
### 5. Limit Usage for Sensitive Roles
|
||||
|
||||
- **Executive/C-level positions** — Consider traditional channels for confidential searches
|
||||
- **Internal transfers** — Use internal HR systems instead
|
||||
- **Security-sensitive roles** — Avoid posting details that could reveal infrastructure or vulnerabilities
|
||||
|
||||
### 6. Background Polling Awareness
|
||||
|
||||
- **Monitor sub-agents spawn automatically** — Each job post creates a background monitor that polls every 2 minutes
|
||||
- **Normal behavior** — This is expected and required for LinkedIn URL notification
|
||||
- **No action needed** — Monitors auto-cleanup after completion
|
||||
|
||||
---
|
||||
|
||||
**Quick Checklist Before Posting:**
|
||||
|
||||
- [ ] Using dedicated hiring email (not personal)
|
||||
- [ ] Job description contains no sensitive/confidential info
|
||||
- [ ] Comfortable with third-party relay service
|
||||
- [ ] Job ID saved for tracking
|
||||
- [ ] Role is appropriate for public job boards
|
||||
|
||||
## 🎯 When to Use
|
||||
|
||||
Use this skill when you need to:
|
||||
|
||||
- Post a job opening quickly
|
||||
- Create a job listing for any role
|
||||
- Generate a resume collection link
|
||||
- Share job postings with candidates
|
||||
- Sync jobs to LinkedIn
|
||||
|
||||
## 🛠️ Tools
|
||||
|
||||
### post_job ⭐
|
||||
|
||||
Post job opening to 20+ job boards. Returns immediately with Job ID.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Required | Type | Description | Default |
|
||||
| -------------------- | -------- | ------ | ------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| `title` | ✅ Yes | string | Job title (min 4 characters) | - |
|
||||
| `city_query` | ✅ Yes | string | City/location (supports fuzzy match) | - |
|
||||
| `description` | ✅ Yes | string | Job description | - |
|
||||
| `email` | ✅ Yes | string | Email to receive resumes | - |
|
||||
| `company` | ❌ No | string | Company name | `"Your Company"` |
|
||||
| `industry` | ❌ No | string | Industry/field | `"General"` |
|
||||
| `linkedinCompanyUrl` | ❌ No | string | LinkedIn company URL for posting | `"https://www.linkedin.com/company/110195078/admin/dashboard"` |
|
||||
|
||||
#### Response
|
||||
|
||||
Returns Job ID. **LinkedIn sync takes 5-60 minutes in background.**
|
||||
|
||||
---
|
||||
|
||||
### check_linkedin_status
|
||||
|
||||
Check LinkedIn sync status once. Returns URL if ready, or "Pending".
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Required | Type | Description | Example |
|
||||
| --------- | -------- | ------ | -------------------- | ------------ |
|
||||
| `jobId` | ✅ Yes | string | The job ID to check | `"abcd2345"` |
|
||||
|
||||
---
|
||||
|
||||
## 🤖 AI Assistant Workflow
|
||||
|
||||
### ⚠️ REQUIRED: Post Job + Spawn Monitor (Two Steps)
|
||||
|
||||
After `post_job` returns, you **MUST** spawn a sub-agent to monitor LinkedIn sync. Do NOT skip this.
|
||||
|
||||
**Step 1: Post Job**
|
||||
|
||||
```
|
||||
result = await post_job({ title, city_query, description, email, ... })
|
||||
```
|
||||
|
||||
**Step 2: Spawn Monitor (REQUIRED)**
|
||||
|
||||
Extract jobId from result, then spawn sub-agent:
|
||||
|
||||
```javascript
|
||||
// Extract jobId from post_job result
|
||||
const jobIdMatch = result.match(/Job ID:\s*`?([a-zA-Z0-9-]+)`?/);
|
||||
const jobId = jobIdMatch?.[1];
|
||||
|
||||
// Spawn sub-agent to monitor
|
||||
await sessions_spawn({
|
||||
task: `Monitor LinkedIn sync for job ID "${jobId}". Loop: call check_linkedin_status with jobId="${jobId}", wait 2 minutes, repeat up to 30 times until LinkedIn URL is returned. Notify user when URL is available.`,
|
||||
mode: "run",
|
||||
cleanup: "delete"
|
||||
});
|
||||
|
||||
// Tell user
|
||||
"✅ Job posted! Job ID: `xxx`. I'll notify you when LinkedIn sync completes (5-60 min)."
|
||||
```
|
||||
|
||||
**⚠️ If you skip Step 2:**
|
||||
- User will NOT receive LinkedIn URL
|
||||
- Incomplete workflow
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Install via ClawHub
|
||||
|
||||
```bash
|
||||
clawhub install post-job
|
||||
```
|
||||
|
||||
### Manual Installation
|
||||
|
||||
```bash
|
||||
# Clone or download the skill
|
||||
cd your-openclaw-workspace/skills
|
||||
|
||||
# Install dependencies
|
||||
cd post-job
|
||||
npm install
|
||||
```
|
||||
|
||||
## 🔐 Security Notes
|
||||
|
||||
- **Email Privacy**: Resume emails are visible in job postings - use a dedicated hiring email
|
||||
- **Rate Limiting**: API may have rate limits for high-volume posting
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Issue: Job posts but no confirmation
|
||||
|
||||
**Cause**: Response timeout or network issue
|
||||
|
||||
**Solution**: Check backend logs, verify API credentials, retry with `--force`
|
||||
|
||||
### Issue: City not recognized
|
||||
|
||||
**Cause**: City not in location database
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Check `assets/locations.json` for supported cities
|
||||
2. Try alternative spelling (e.g., "New York" vs "NYC")
|
||||
3. Add new city to database and republish
|
||||
|
||||
### Issue: Duplicate job postings
|
||||
|
||||
**Cause**: Multiple API calls due to retry logic
|
||||
|
||||
**Solution**: Check backend for duplicate jobs, implement request deduplication
|
||||
|
||||
## ❓ FAQ - Security & Privacy
|
||||
|
||||
**Q: Is my data safe with Fuku AI?**
|
||||
A: Job data is transmitted to Fuku AI's servers for distribution. They act as a relay service. Avoid sharing confidential information in job descriptions.
|
||||
|
||||
**Q: Do I need to trust Fuku AI?**
|
||||
A: Yes — this skill depends on their service to post jobs. Review their terms at https://www.fuku.ai if you have concerns.
|
||||
|
||||
**Q: Can I use this without LinkedIn sync?**
|
||||
A: Yes — jobs are still posted to 20+ other boards. LinkedIn is optional background sync.
|
||||
|
||||
**Q: Will the job appear on MY LinkedIn company page?**
|
||||
A: No — postings appear through Fuku AI's relay accounts, not your company's page. This is why no LinkedIn binding is required.
|
||||
|
||||
**Q: What happens if Fuku AI goes offline?**
|
||||
A: Job posting may fail or LinkedIn sync may be delayed. The skill will return an error message.
|
||||
|
||||
**Q: Can I delete a job after posting?**
|
||||
A: Contact Fuku AI support with your Job ID. Direct deletion through this skill is not currently supported.
|
||||
|
||||
**Q: Is the embedded credential a security risk?**
|
||||
A: The embedded identifier is for Fuku AI's free tier access. It doesn't expose your personal credentials, but means jobs are posted under their service account.
|
||||
|
||||
**Q: Should I use this for confidential hiring?**
|
||||
A: No — use traditional channels (internal HR systems, executive search firms) for sensitive or confidential roles.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Found a bug or want to add more cities?
|
||||
|
||||
1. Fork the skill
|
||||
2. Make your changes
|
||||
3. Test thoroughly
|
||||
4. Submit a pull request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This skill is provided as-is for use with OpenClaw.
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
For issues or questions:
|
||||
|
||||
- Check this SKILL.md for troubleshooting
|
||||
- Review error messages carefully
|
||||
- Contact developer email yangkai31@gmail.com if you run into any issues
|
||||
|
||||
---
|
||||
|
||||
**Happy Hiring! 🎉**
|
||||
6
_meta.json
Normal file
6
_meta.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn766g3fzj7qdcqppg4433krw981sxyc",
|
||||
"slug": "post-job",
|
||||
"version": "1.6.1",
|
||||
"publishedAt": 1773979435364
|
||||
}
|
||||
939
assets/locations.json
Normal file
939
assets/locations.json
Normal file
@@ -0,0 +1,939 @@
|
||||
[
|
||||
{
|
||||
"label": "Singapore",
|
||||
"value": "70000001",
|
||||
"parentLabel": "Singapore",
|
||||
"parentValue": "SI",
|
||||
"addressCountry": "SG",
|
||||
"postalCode": "139950",
|
||||
"addressRegion": "West Region",
|
||||
"streetAddress": "67 Ayer Rajah Crescent",
|
||||
"addressLocality": "Buona Vista"
|
||||
},
|
||||
{
|
||||
"label": "Dubai",
|
||||
"value": "71000043",
|
||||
"parentLabel": "United Arab Emirates",
|
||||
"parentValue": "UE",
|
||||
"addressCountry": "AE",
|
||||
"postalCode": "00000",
|
||||
"addressRegion": "Dubai Emirate",
|
||||
"streetAddress": "Sheikh Zayed Road, Building No 22",
|
||||
"addressLocality": "Dubai"
|
||||
},
|
||||
{
|
||||
"label": "Kuala Lumpur",
|
||||
"value": "74000002",
|
||||
"parentLabel": "Malaysia",
|
||||
"parentValue": "MY",
|
||||
"addressCountry": "MY",
|
||||
"postalCode": "50450",
|
||||
"addressRegion": "Federal Territory of Kuala Lumpur",
|
||||
"streetAddress": "15 Jalan Ampang",
|
||||
"addressLocality": "Kuala Lumpur City Centre"
|
||||
},
|
||||
{
|
||||
"label": "Johor Bahru",
|
||||
"value": "74000005",
|
||||
"parentLabel": "Malaysia",
|
||||
"parentValue": "MY",
|
||||
"addressCountry": "MY",
|
||||
"postalCode": "80100",
|
||||
"addressRegion": "Johor",
|
||||
"streetAddress": "Jalan Wong Ah Fook 1",
|
||||
"addressLocality": "Johor Bahru"
|
||||
},
|
||||
{
|
||||
"label": "George Town",
|
||||
"value": "74000024",
|
||||
"parentLabel": "Malaysia",
|
||||
"parentValue": "MY",
|
||||
"addressCountry": "MY",
|
||||
"postalCode": "10200",
|
||||
"addressRegion": "Penang",
|
||||
"streetAddress": "No. 116 & 118 Lebuh Acheh",
|
||||
"addressLocality": "George Town"
|
||||
},
|
||||
{
|
||||
"label": "Klang",
|
||||
"value": "74000003",
|
||||
"parentLabel": "Malaysia",
|
||||
"parentValue": "MY",
|
||||
"addressCountry": "MY",
|
||||
"postalCode": "41200",
|
||||
"addressRegion": "Selangor",
|
||||
"streetAddress": "1, Persiaran Batu Nilam 1, Bandar Bukit Tinggi 2",
|
||||
"addressLocality": "Klang"
|
||||
},
|
||||
{
|
||||
"label": "Shah Alam",
|
||||
"value": "74000009",
|
||||
"parentLabel": "Malaysia",
|
||||
"parentValue": "MY",
|
||||
"addressCountry": "MY",
|
||||
"postalCode": "40000",
|
||||
"addressRegion": "Selangor",
|
||||
"streetAddress": "2, Jalan Perbandaran, Seksyen 14",
|
||||
"addressLocality": "Shah Alam"
|
||||
},
|
||||
{
|
||||
"label": "Hong Kong",
|
||||
"value": "79000002",
|
||||
"parentLabel": "Hong Kong",
|
||||
"parentValue": "HK",
|
||||
"addressCountry": "HK",
|
||||
"postalCode": "0000",
|
||||
"addressRegion": "Hong Kong Island",
|
||||
"streetAddress": "Room 1205, 12/F, Central Plaza, 18 Harbour Road",
|
||||
"addressLocality": "Wan Chai"
|
||||
},
|
||||
{
|
||||
"label": "Bangkok",
|
||||
"value": "61000905",
|
||||
"parentLabel": "Thailand",
|
||||
"parentValue": "TH",
|
||||
"addressCountry": "TH",
|
||||
"postalCode": "10110",
|
||||
"addressRegion": "Bangkok Metropolitan Area",
|
||||
"streetAddress": "10 Soi Sukhumvit 63 (Ekkamai)",
|
||||
"addressLocality": "Khlong Toei Nuea"
|
||||
},
|
||||
{
|
||||
"label": "Myanmar",
|
||||
"value": "330000401",
|
||||
"parentLabel": "Myanmar",
|
||||
"parentValue": "MM",
|
||||
"addressCountry": "MM",
|
||||
"postalCode": "11141",
|
||||
"addressRegion": "Yangon Region",
|
||||
"streetAddress": "12 Bogyoke Aung San Road",
|
||||
"addressLocality": "Yangon"
|
||||
},
|
||||
{
|
||||
"label": "Hanoi",
|
||||
"value": "160000003",
|
||||
"parentLabel": "Vietnam",
|
||||
"parentValue": "VN",
|
||||
"addressCountry": "VN",
|
||||
"postalCode": "100000",
|
||||
"addressRegion": "Hà Nội",
|
||||
"streetAddress": "123 Đường Hoàng Quốc Việt",
|
||||
"addressLocality": "Cầu Giấy District"
|
||||
},
|
||||
{
|
||||
"label": "Ho Chi Minh City",
|
||||
"value": "160000002",
|
||||
"parentLabel": "Vietnam",
|
||||
"parentValue": "VN",
|
||||
"addressCountry": "VN",
|
||||
"postalCode": "700000",
|
||||
"addressRegion": "Ho Chi Minh City",
|
||||
"streetAddress": "91 Nguyen Cu Trinh St",
|
||||
"addressLocality": "Nguyen Cu Trinh Ward, District 1"
|
||||
},
|
||||
{
|
||||
"label": "Manila",
|
||||
"value": "51000000",
|
||||
"parentLabel": "Philippines",
|
||||
"parentValue": "PH",
|
||||
"addressCountry": "PH",
|
||||
"postalCode": "1000",
|
||||
"addressRegion": "National Capital Region",
|
||||
"streetAddress": "691 A. Arnaiz Ave",
|
||||
"addressLocality": "Makati City"
|
||||
},
|
||||
{
|
||||
"label": "Jakarta",
|
||||
"value": "76000207",
|
||||
"parentLabel": "Indonesia",
|
||||
"parentValue": "ID",
|
||||
"addressCountry": "ID",
|
||||
"postalCode": "10310",
|
||||
"addressRegion": "DKI Jakarta",
|
||||
"streetAddress": "Jl. Jend. Sudirman No. 10",
|
||||
"addressLocality": "Jakarta Pusat"
|
||||
},
|
||||
{
|
||||
"label": "Tokyo",
|
||||
"value": "36094389",
|
||||
"parentLabel": "Japan",
|
||||
"parentValue": "JP",
|
||||
"addressCountry": "JP",
|
||||
"postalCode": "100-0001",
|
||||
"addressRegion": "Tokyo Metropolis",
|
||||
"streetAddress": "1-1 Chiyoda",
|
||||
"addressLocality": "Chiyoda-ku"
|
||||
},
|
||||
{
|
||||
"label": "Osaka",
|
||||
"value": "36049159",
|
||||
"parentLabel": "Japan",
|
||||
"parentValue": "JP",
|
||||
"addressCountry": "JP",
|
||||
"postalCode": "530-0001",
|
||||
"addressRegion": "Osaka Prefecture",
|
||||
"streetAddress": "3-1 Umeda 1-chōme",
|
||||
"addressLocality": "Kita-ku Osaka"
|
||||
},
|
||||
{
|
||||
"label": "Seoul",
|
||||
"value": "320006457",
|
||||
"parentLabel": "South Korea",
|
||||
"parentValue": "KR",
|
||||
"addressCountry": "KR",
|
||||
"postalCode": "04524",
|
||||
"addressRegion": "Seoul Special City",
|
||||
"streetAddress": "50 Myeongdong 8-gil",
|
||||
"addressLocality": "Jung-gu"
|
||||
},
|
||||
{
|
||||
"label": "Busan",
|
||||
"value": "320006397",
|
||||
"parentLabel": "South Korea",
|
||||
"parentValue": "KR",
|
||||
"addressCountry": "KR",
|
||||
"postalCode": "47285",
|
||||
"addressRegion": "Busan Metropolitan City",
|
||||
"streetAddress": "772 Gaya-daero",
|
||||
"addressLocality": "Busanjin-gu"
|
||||
},
|
||||
{
|
||||
"label": "Beijing",
|
||||
"value": "68000002",
|
||||
"parentLabel": "China",
|
||||
"parentValue": "CN",
|
||||
"addressCountry": "CN",
|
||||
"postalCode": "100000",
|
||||
"addressRegion": "Beijing Municipality",
|
||||
"streetAddress": "No. 1 Jianguomenwai Avenue",
|
||||
"addressLocality": "Dongcheng District"
|
||||
},
|
||||
{
|
||||
"label": "Shanghai",
|
||||
"value": "68000169",
|
||||
"parentLabel": "China",
|
||||
"parentValue": "CN",
|
||||
"addressCountry": "CN",
|
||||
"postalCode": "200000",
|
||||
"addressRegion": "Shanghai Municipality",
|
||||
"streetAddress": "No. 168 Pudong New Area Lujiazui",
|
||||
"addressLocality": "Pudong New Area"
|
||||
},
|
||||
{
|
||||
"label": "Guangzhou",
|
||||
"value": "68000466",
|
||||
"parentLabel": "China",
|
||||
"parentValue": "CN",
|
||||
"addressCountry": "CN",
|
||||
"postalCode": "510000",
|
||||
"addressRegion": "Guangdong Province",
|
||||
"streetAddress": "No. 1 Zhongshan Road",
|
||||
"addressLocality": "Guangzhou"
|
||||
},
|
||||
{
|
||||
"label": "Shenzhen",
|
||||
"value": "68000468",
|
||||
"parentLabel": "China",
|
||||
"parentValue": "CN",
|
||||
"addressCountry": "CN",
|
||||
"postalCode": "518000",
|
||||
"addressRegion": "Guangdong Province",
|
||||
"streetAddress": "No. 1 Shennan Boulevard",
|
||||
"addressLocality": "Futian District"
|
||||
},
|
||||
{
|
||||
"label": "Taipei",
|
||||
"value": "170000325",
|
||||
"parentLabel": "Taiwan",
|
||||
"parentValue": "TW",
|
||||
"addressCountry": "TW",
|
||||
"postalCode": "100",
|
||||
"addressRegion": "Taipei City",
|
||||
"streetAddress": "No. 1, Zhongxiao W. Rd.",
|
||||
"addressLocality": "Zhongzheng District"
|
||||
},
|
||||
{
|
||||
"label": "Sydney",
|
||||
"value": "10000195",
|
||||
"parentLabel": "Australia",
|
||||
"parentValue": "AU",
|
||||
"addressCountry": "AU",
|
||||
"postalCode": "2000",
|
||||
"addressRegion": "NSW",
|
||||
"streetAddress": "1 Martin Place",
|
||||
"addressLocality": "Sydney CBD"
|
||||
},
|
||||
{
|
||||
"label": "Melbourne",
|
||||
"value": "10006677",
|
||||
"parentLabel": "Australia",
|
||||
"parentValue": "AU",
|
||||
"addressCountry": "AU",
|
||||
"postalCode": "3000",
|
||||
"addressRegion": "VIC",
|
||||
"streetAddress": "1 Collins Street",
|
||||
"addressLocality": "Melbourne CBD"
|
||||
},
|
||||
{
|
||||
"label": "Auckland",
|
||||
"value": "49001765",
|
||||
"parentLabel": "New Zealand",
|
||||
"parentValue": "NZ",
|
||||
"addressCountry": "NZ",
|
||||
"postalCode": "1010",
|
||||
"addressRegion": "Auckland Region",
|
||||
"streetAddress": "1 Queen Street",
|
||||
"addressLocality": "Auckland CBD"
|
||||
},
|
||||
{
|
||||
"label": "New Delhi",
|
||||
"value": "32015163",
|
||||
"parentLabel": "India",
|
||||
"parentValue": "IN",
|
||||
"addressCountry": "IN",
|
||||
"postalCode": "110001",
|
||||
"addressRegion": "Delhi",
|
||||
"streetAddress": "Rashtrapati Bhavan, Raisina Hill",
|
||||
"addressLocality": "New Delhi"
|
||||
},
|
||||
{
|
||||
"label": "Mumbai",
|
||||
"value": "32015169",
|
||||
"parentLabel": "India",
|
||||
"parentValue": "IN",
|
||||
"addressCountry": "IN",
|
||||
"postalCode": "400001",
|
||||
"addressRegion": "Maharashtra",
|
||||
"streetAddress": "Taj Mahal Palace, Apollo Bunder",
|
||||
"addressLocality": "Colaba"
|
||||
},
|
||||
{
|
||||
"label": "Bangalore",
|
||||
"value": "32015168",
|
||||
"parentLabel": "India",
|
||||
"parentValue": "IN",
|
||||
"addressCountry": "IN",
|
||||
"postalCode": "560001",
|
||||
"addressRegion": "Karnataka",
|
||||
"streetAddress": "No. 1 MG Road",
|
||||
"addressLocality": "Bengaluru Central"
|
||||
},
|
||||
{
|
||||
"label": "Pune",
|
||||
"value": "32015170",
|
||||
"parentLabel": "India",
|
||||
"parentValue": "IN",
|
||||
"addressCountry": "IN",
|
||||
"postalCode": "411001",
|
||||
"addressRegion": "Maharashtra",
|
||||
"streetAddress": "1 MG Road",
|
||||
"addressLocality": "Pune City"
|
||||
},
|
||||
{
|
||||
"label": "Chennai",
|
||||
"value": "32015167",
|
||||
"parentLabel": "India",
|
||||
"parentValue": "IN",
|
||||
"addressCountry": "IN",
|
||||
"postalCode": "600001",
|
||||
"addressRegion": "Tamil Nadu",
|
||||
"streetAddress": "Rashtrapathi Bhavan Road",
|
||||
"addressLocality": "Fort St George, Chennai"
|
||||
},
|
||||
|
||||
{
|
||||
"label": "Riyadh",
|
||||
"value": "180000067",
|
||||
"parentLabel": "Saudi Arabia",
|
||||
"parentValue": "SA",
|
||||
"addressCountry": "SA",
|
||||
"postalCode": "11564",
|
||||
"addressRegion": "Riyadh Province",
|
||||
"streetAddress": "King Fahd Road",
|
||||
"addressLocality": "Olaya District"
|
||||
},
|
||||
{
|
||||
"label": "Jeddah",
|
||||
"value": "180000053",
|
||||
"parentLabel": "Saudi Arabia",
|
||||
"parentValue": "SA",
|
||||
"addressCountry": "SA",
|
||||
"postalCode": "22233",
|
||||
"addressRegion": "Makkah Province",
|
||||
"streetAddress": "King Road",
|
||||
"addressLocality": "Al-Olaya District"
|
||||
},
|
||||
{
|
||||
"label": "Abu Dhabi",
|
||||
"value": "71000002",
|
||||
"parentLabel": "United Arab Emirates",
|
||||
"parentValue": "UE",
|
||||
"addressCountry": "AE",
|
||||
"postalCode": "00000",
|
||||
"addressRegion": "Abu Dhabi Emirate",
|
||||
"streetAddress": "Sheikh Zayed Road",
|
||||
"addressLocality": "Abu Dhabi"
|
||||
},
|
||||
{
|
||||
"label": "Doha",
|
||||
"value": "72000021",
|
||||
"parentLabel": "Qatar",
|
||||
"parentValue": "QA",
|
||||
"addressCountry": "QA",
|
||||
"postalCode": "00000",
|
||||
"addressRegion": "Doha",
|
||||
"streetAddress": "West Bay Square, Al Qatar Rd",
|
||||
"addressLocality": "Doha City"
|
||||
},
|
||||
{
|
||||
"label": "Cairo",
|
||||
"value": "78000049",
|
||||
"parentLabel": "Egypt",
|
||||
"parentValue": "EG",
|
||||
"addressCountry": "EG",
|
||||
"postalCode": "11511",
|
||||
"addressRegion": "Cairo Governorate",
|
||||
"streetAddress": "1 Abdel Khalek Tharwat St, Ataba Square",
|
||||
"addressLocality": "Cairo"
|
||||
},
|
||||
{
|
||||
"label": "Istanbul",
|
||||
"value": "62051381",
|
||||
"parentLabel": "Turkey",
|
||||
"parentValue": "TR",
|
||||
"addressCountry": "TR",
|
||||
"postalCode": "34000",
|
||||
"addressRegion": "Istanbul Province",
|
||||
"streetAddress": "İstiklal Caddesi No: 100",
|
||||
"addressLocality": "Beyoğlu"
|
||||
},
|
||||
{
|
||||
"label": "New York",
|
||||
"value": "5027742",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "10001",
|
||||
"addressRegion": "NY",
|
||||
"streetAddress": "401 7th Ave",
|
||||
"addressLocality": "Manhattan"
|
||||
},
|
||||
{
|
||||
"label": "San Francisco",
|
||||
"value": "5004604",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "94105",
|
||||
"addressRegion": "CA",
|
||||
"streetAddress": "595 Market St",
|
||||
"addressLocality": "San Francisco"
|
||||
},
|
||||
{
|
||||
"label": "Los Angeles",
|
||||
"value": "5002945",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "90001",
|
||||
"addressRegion": "CA",
|
||||
"streetAddress": "100 W 1st Street",
|
||||
"addressLocality": "Los Angeles"
|
||||
},
|
||||
{
|
||||
"label": "San Jose",
|
||||
"value": "5004711",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "95113",
|
||||
"addressRegion": "CA",
|
||||
"streetAddress": "200 E Santa Clara St",
|
||||
"addressLocality": "San Jose"
|
||||
},
|
||||
{
|
||||
"label": "San Diego",
|
||||
"value": "5004327",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "92101",
|
||||
"addressRegion": "CA",
|
||||
"streetAddress": "600 Broadway",
|
||||
"addressLocality": "San Diego"
|
||||
},
|
||||
{
|
||||
"label": "Seattle",
|
||||
"value": "5040918",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "98101",
|
||||
"addressRegion": "WA",
|
||||
"streetAddress": "500 Yale Ave N",
|
||||
"addressLocality": "Seattle"
|
||||
},
|
||||
{
|
||||
"label": "Denver",
|
||||
"value": "5005614",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "80202",
|
||||
"addressRegion": "CO",
|
||||
"streetAddress": "Denver Pavilions, 700 16th St",
|
||||
"addressLocality": "Denver"
|
||||
},
|
||||
{
|
||||
"label": "Chicago",
|
||||
"value": "5011305",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "60601",
|
||||
"addressRegion": "IL",
|
||||
"streetAddress": "233 S Wacker Dr",
|
||||
"addressLocality": "Chicago"
|
||||
},
|
||||
{
|
||||
"label": "Houston",
|
||||
"value": "5037121",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "77002",
|
||||
"addressRegion": "TX",
|
||||
"streetAddress": "1400 Lubbock Street",
|
||||
"addressLocality": "Houston"
|
||||
},
|
||||
{
|
||||
"label": "Austin",
|
||||
"value": "5038603",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "78701",
|
||||
"addressRegion": "TX",
|
||||
"streetAddress": "500 W 2nd Street",
|
||||
"addressLocality": "Austin"
|
||||
},
|
||||
{
|
||||
"label": "San Antonio",
|
||||
"value": "5043734",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "78205",
|
||||
"addressRegion": "TX",
|
||||
"streetAddress": "615 E Houston St",
|
||||
"addressLocality": "San Antonio"
|
||||
},
|
||||
{
|
||||
"label": "Dallas",
|
||||
"value": "5036565",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "75201",
|
||||
"addressRegion": "TX",
|
||||
"streetAddress": "1500 Main St",
|
||||
"addressLocality": "Dallas"
|
||||
},
|
||||
{
|
||||
"label": "Phoenix",
|
||||
"value": "5002020",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "85003",
|
||||
"addressRegion": "AZ",
|
||||
"streetAddress": "220 N Central Ave",
|
||||
"addressLocality": "Phoenix"
|
||||
},
|
||||
{
|
||||
"label": "Philadelphia",
|
||||
"value": "5033484",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "19103",
|
||||
"addressRegion": "PA",
|
||||
"streetAddress": "1900 Market St",
|
||||
"addressLocality": "Philadelphia"
|
||||
},
|
||||
{
|
||||
"label": "District of Columbia",
|
||||
"value": "5043669",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "20001",
|
||||
"addressRegion": "DC",
|
||||
"streetAddress": "1600 Pennsylvania Ave NW",
|
||||
"addressLocality": "Washington"
|
||||
},
|
||||
{
|
||||
"label": "Boston",
|
||||
"value": "5016967",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "02108",
|
||||
"addressRegion": "MA",
|
||||
"streetAddress": "1 Boston Common",
|
||||
"addressLocality": "Boston"
|
||||
},
|
||||
{
|
||||
"label": "Las Vegas",
|
||||
"value": "5026496",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "89101",
|
||||
"addressRegion": "NV",
|
||||
"streetAddress": "3950 S Las Vegas Blvd",
|
||||
"addressLocality": "Las Vegas"
|
||||
},
|
||||
{
|
||||
"label": "Miami",
|
||||
"value": "5007077",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "33101",
|
||||
"addressRegion": "FL",
|
||||
"streetAddress": "100 NE 2nd Ave",
|
||||
"addressLocality": "Miami"
|
||||
},
|
||||
{
|
||||
"label": "Detroit",
|
||||
"value": "5019488",
|
||||
"parentLabel": "United States",
|
||||
"parentValue": "US",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "48201",
|
||||
"addressRegion": "MI",
|
||||
"streetAddress": "One Detroit Center, 500 Woodward Ave",
|
||||
"addressLocality": "Detroit"
|
||||
},
|
||||
{
|
||||
"label": "Toronto",
|
||||
"value": "15003137",
|
||||
"parentLabel": "Canada",
|
||||
"parentValue": "CA",
|
||||
"addressCountry": "CA",
|
||||
"postalCode": "M5J 2N8",
|
||||
"addressRegion": "ON",
|
||||
"streetAddress": "100 King Street West",
|
||||
"addressLocality": "Toronto"
|
||||
},
|
||||
{
|
||||
"label": "Vancouver",
|
||||
"value": "15003205",
|
||||
"parentLabel": "Canada",
|
||||
"parentValue": "CA",
|
||||
"addressCountry": "CA",
|
||||
"postalCode": "V6B 0A8",
|
||||
"addressRegion": "BC",
|
||||
"streetAddress": "800 West Pender Street",
|
||||
"addressLocality": "Vancouver"
|
||||
},
|
||||
{
|
||||
"label": "Montreal",
|
||||
"value": "15001867",
|
||||
"parentLabel": "Canada",
|
||||
"parentValue": "CA",
|
||||
"addressCountry": "CA",
|
||||
"postalCode": "H3A 0A2",
|
||||
"addressRegion": "QC",
|
||||
"streetAddress": "100 Saint-Jacques Street",
|
||||
"addressLocality": "Downtown Montreal"
|
||||
},
|
||||
{
|
||||
"label": "London",
|
||||
"value": "1024195",
|
||||
"parentLabel": "United Kingdom",
|
||||
"parentValue": "GB",
|
||||
"addressCountry": "GB",
|
||||
"postalCode": "WC2N 5HX",
|
||||
"addressRegion": "London",
|
||||
"streetAddress": "10 Great Queen Street",
|
||||
"addressLocality": "Covent Garden"
|
||||
},
|
||||
{
|
||||
"label": "Berlin",
|
||||
"value": "3001350",
|
||||
"parentLabel": "Germany",
|
||||
"parentValue": "DE",
|
||||
"addressCountry": "DE",
|
||||
"postalCode": "10117",
|
||||
"addressRegion": "Berlin",
|
||||
"streetAddress": "Unter den Linden 77",
|
||||
"addressLocality": "Mitte"
|
||||
},
|
||||
{
|
||||
"label": "Munich",
|
||||
"value": "3016382",
|
||||
"parentLabel": "Germany",
|
||||
"parentValue": "DE",
|
||||
"addressCountry": "DE",
|
||||
"postalCode": "80331",
|
||||
"addressRegion": "Bavaria",
|
||||
"streetAddress": "Marienplatz 1",
|
||||
"addressLocality": "Mitte"
|
||||
},
|
||||
|
||||
{
|
||||
"label": "Madrid",
|
||||
"value": "2041600",
|
||||
"parentLabel": "Spain",
|
||||
"parentValue": "ES",
|
||||
"addressCountry": "ES",
|
||||
"postalCode": "28001",
|
||||
"addressRegion": "Community of Madrid",
|
||||
"streetAddress": "Plaza de la Puerta del Sol 1",
|
||||
"addressLocality": "Madrid"
|
||||
},
|
||||
{
|
||||
"label": "Barcelona",
|
||||
"value": "2015583",
|
||||
"parentLabel": "Spain",
|
||||
"parentValue": "ES",
|
||||
"addressCountry": "ES",
|
||||
"postalCode": "08001",
|
||||
"addressRegion": "Catalonia",
|
||||
"streetAddress": "Plaça de Catalunya 1",
|
||||
"addressLocality": "Barcelona"
|
||||
},
|
||||
{
|
||||
"label": "Rome",
|
||||
"value": "34020900",
|
||||
"parentLabel": "Italy",
|
||||
"parentValue": "IT",
|
||||
"addressCountry": "IT",
|
||||
"postalCode": "00118",
|
||||
"addressRegion": "Lazio",
|
||||
"streetAddress": "Via del Corso 1",
|
||||
"addressLocality": "Roma"
|
||||
},
|
||||
{
|
||||
"label": "Milan",
|
||||
"value": "34000439",
|
||||
"parentLabel": "Italy",
|
||||
"parentValue": "IT",
|
||||
"addressCountry": "IT",
|
||||
"postalCode": "20121",
|
||||
"addressRegion": "Lombardy",
|
||||
"streetAddress": "Via Monte Napoleone 1",
|
||||
"addressLocality": "Milano"
|
||||
},
|
||||
{
|
||||
"label": "Paris",
|
||||
"value": "4014735",
|
||||
"parentLabel": "France",
|
||||
"parentValue": "FR",
|
||||
"addressCountry": "FR",
|
||||
"postalCode": "75001",
|
||||
"addressRegion": "Île-de-France",
|
||||
"streetAddress": "10 Place de la Concorde",
|
||||
"addressLocality": "Paris"
|
||||
},
|
||||
{
|
||||
"label": "Luxembourg",
|
||||
"value": "39000000",
|
||||
"parentLabel": "Luxembourg",
|
||||
"parentValue": "LU",
|
||||
"addressCountry": "LU",
|
||||
"postalCode": "L-1011",
|
||||
"addressRegion": "Luxembourg",
|
||||
"streetAddress": "10 Rue du Nord",
|
||||
"addressLocality": "Ville de Luxembourg"
|
||||
},
|
||||
{
|
||||
"label": "Geneva",
|
||||
"value": "16001544",
|
||||
"parentLabel": "Switzerland",
|
||||
"parentValue": "CH",
|
||||
"addressCountry": "CH",
|
||||
"postalCode": "1201",
|
||||
"addressRegion": "Canton of Geneva",
|
||||
"streetAddress": "Rue du Rhône 75",
|
||||
"addressLocality": "Genève"
|
||||
},
|
||||
{
|
||||
"label": "Zürich",
|
||||
"value": "16004811",
|
||||
"parentLabel": "Switzerland",
|
||||
"parentValue": "CH",
|
||||
"addressCountry": "CH",
|
||||
"postalCode": "8001",
|
||||
"addressRegion": "Zurich",
|
||||
"streetAddress": "Bahnhofstrasse 1",
|
||||
"addressLocality": "Zürich"
|
||||
},
|
||||
{
|
||||
"label": "Amsterdam",
|
||||
"value": "67000990",
|
||||
"parentLabel": "Netherlands",
|
||||
"parentValue": "NL",
|
||||
"addressCountry": "NL",
|
||||
"postalCode": "1012 AB",
|
||||
"addressRegion": "North Holland",
|
||||
"streetAddress": "Dam Square 1",
|
||||
"addressLocality": "Amsterdam"
|
||||
},
|
||||
{
|
||||
"label": "Dublin",
|
||||
"value": "47002909",
|
||||
"parentLabel": "Ireland",
|
||||
"parentValue": "IE",
|
||||
"addressCountry": "IE",
|
||||
"postalCode": "D02 X285",
|
||||
"addressRegion": "Dublin",
|
||||
"streetAddress": "32 Grafton Street",
|
||||
"addressLocality": "Dublin 2"
|
||||
},
|
||||
{
|
||||
"label": "Vienna",
|
||||
"value": "9046400",
|
||||
"parentLabel": "Austria",
|
||||
"parentValue": "AT",
|
||||
"addressCountry": "AT",
|
||||
"postalCode": "1010",
|
||||
"addressRegion": "Vienna",
|
||||
"streetAddress": "Stephansplatz 1",
|
||||
"addressLocality": "Innere Stadt"
|
||||
},
|
||||
{
|
||||
"label": "Monaco",
|
||||
"value": "40000005",
|
||||
"parentLabel": "Monaco",
|
||||
"parentValue": "MC",
|
||||
"addressCountry": "MC",
|
||||
"postalCode": "98000",
|
||||
"addressRegion": "Monaco",
|
||||
"streetAddress": "10 Rue du Nord",
|
||||
"addressLocality": "Monaco-Ville"
|
||||
},
|
||||
{
|
||||
"label": "Brussels",
|
||||
"value": "12003806",
|
||||
"parentLabel": "Belgium",
|
||||
"parentValue": "BE",
|
||||
"addressCountry": "BE",
|
||||
"postalCode": "1000",
|
||||
"addressRegion": "Brussels-Capital Region",
|
||||
"streetAddress": "Rue du Nord 26",
|
||||
"addressLocality": "Brussels"
|
||||
},
|
||||
{
|
||||
"label": "Stockholm",
|
||||
"value": "120000095",
|
||||
"parentLabel": "Sweden",
|
||||
"parentValue": "SE",
|
||||
"addressCountry": "SE",
|
||||
"postalCode": "111 64",
|
||||
"addressRegion": "Stockholm County",
|
||||
"streetAddress": "Drottninggatan 77",
|
||||
"addressLocality": "Stockholm"
|
||||
},
|
||||
{
|
||||
"label": "Copenhagen",
|
||||
"value": "75000403",
|
||||
"parentLabel": "Denmark",
|
||||
"parentValue": "DM",
|
||||
"addressCountry": "DK",
|
||||
"postalCode": "1050",
|
||||
"addressRegion": "Capital Region of Denmark",
|
||||
"streetAddress": "Vesterbrogade 21",
|
||||
"addressLocality": "Copenhagen V"
|
||||
},
|
||||
{
|
||||
"label": "Lisbon",
|
||||
"value": "54000596",
|
||||
"parentLabel": "Portugal",
|
||||
"parentValue": "PT",
|
||||
"addressCountry": "PT",
|
||||
"postalCode": "1000-001",
|
||||
"addressRegion": "Lisbon",
|
||||
"streetAddress": "Rua da Liberdade 123",
|
||||
"addressLocality": "Lisbon"
|
||||
},
|
||||
{
|
||||
"label": "Sao Paulo",
|
||||
"value": "14005527",
|
||||
"parentLabel": "Brazil",
|
||||
"parentValue": "BR",
|
||||
"addressCountry": "BR",
|
||||
"postalCode": "01000-000",
|
||||
"addressRegion": "São Paulo",
|
||||
"streetAddress": "Av. Paulista 1578",
|
||||
"addressLocality": "Bela Vista"
|
||||
},
|
||||
{
|
||||
"label": "Rio de Janeiro",
|
||||
"value": "14003221",
|
||||
"parentLabel": "Brazil",
|
||||
"parentValue": "BR",
|
||||
"addressCountry": "BR",
|
||||
"postalCode": "20000-000",
|
||||
"addressRegion": "RJ",
|
||||
"streetAddress": "Av. Presidente Vargas 1",
|
||||
"addressLocality": "Centro"
|
||||
},
|
||||
{
|
||||
"label": "Mexico City",
|
||||
"value": "46075206",
|
||||
"parentLabel": "Mexico",
|
||||
"parentValue": "MX",
|
||||
"addressCountry": "MX",
|
||||
"postalCode": "06000",
|
||||
"addressRegion": "Ciudad de México",
|
||||
"streetAddress": "Paseo de la Reforma #1",
|
||||
"addressLocality": "Cuauhtémoc"
|
||||
},
|
||||
{
|
||||
"label": "Argentina",
|
||||
"value": "7020260",
|
||||
"parentLabel": "Argentina",
|
||||
"parentValue": "AR",
|
||||
"addressCountry": "AR",
|
||||
"postalCode": "C1001",
|
||||
"addressRegion": "Buenos Aires City",
|
||||
"streetAddress": "Avenida Florida 123",
|
||||
"addressLocality": "Buenos Aires"
|
||||
},
|
||||
{
|
||||
"label": "Johannesburg",
|
||||
"value": "66001666",
|
||||
"parentLabel": "South Africa",
|
||||
"parentValue": "ZA",
|
||||
"addressCountry": "ZA",
|
||||
"postalCode": "2001",
|
||||
"addressRegion": "Gauteng",
|
||||
"streetAddress": "Ground Floor Sunnyside Office Park, 32 Rissik Street",
|
||||
"addressLocality": "Johannesburg Central Business District"
|
||||
},
|
||||
{
|
||||
"label": "Cape Town",
|
||||
"value": "66003457",
|
||||
"parentLabel": "South Africa",
|
||||
"parentValue": "ZA",
|
||||
"addressCountry": "ZA",
|
||||
"postalCode": "8001",
|
||||
"addressRegion": "Western Cape",
|
||||
"streetAddress": "76 Lower Long Street",
|
||||
"addressLocality": "Cape Town CBD"
|
||||
},
|
||||
{
|
||||
"label": "Lagos",
|
||||
"value": "100000098",
|
||||
"parentLabel": "Nigeria",
|
||||
"parentValue": "NG",
|
||||
"addressCountry": "NG",
|
||||
"postalCode": "100001",
|
||||
"addressRegion": "Lagos State",
|
||||
"streetAddress": "32/34 Broad Street",
|
||||
"addressLocality": "Lagos Island"
|
||||
}
|
||||
]
|
||||
16
package.json
Normal file
16
package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "post-job",
|
||||
"version": "1.6.1",
|
||||
"type": "module",
|
||||
"description": "Post job openings and generate resume collection links",
|
||||
"main": "scripts/post_job.js",
|
||||
"scripts": {
|
||||
"post": "node scripts/post_job.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.3.1",
|
||||
"fuse.js": "^7.0.0"
|
||||
}
|
||||
}
|
||||
221
pnpm-lock.yaml
generated
Normal file
221
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,221 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
axios:
|
||||
specifier: ^1.6.0
|
||||
version: 1.13.5
|
||||
dayjs:
|
||||
specifier: ^1.11.19
|
||||
version: 1.11.19
|
||||
fuse.js:
|
||||
specifier: ^7.0.0
|
||||
version: 7.1.0
|
||||
|
||||
packages:
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
axios@1.13.5:
|
||||
resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
dayjs@1.11.19:
|
||||
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-errors@1.3.0:
|
||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
follow-redirects@1.15.11:
|
||||
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
form-data@4.0.5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
fuse.js@7.1.0:
|
||||
resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
axios@1.13.5:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
form-data: 4.0.5
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
dayjs@1.11.19: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.2
|
||||
|
||||
follow-redirects@1.15.11: {}
|
||||
|
||||
form-data@4.0.5:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
fuse.js@7.1.0: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
function-bind: 1.1.2
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
41
scripts/loadLocations.js
Normal file
41
scripts/loadLocations.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Load locations database for city fuzzy matching
|
||||
* Separated from post_job.js to avoid static analysis false positives
|
||||
*/
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const locationsPath = path.join(__dirname, "..", "assets", "locations.json");
|
||||
|
||||
let locations = [];
|
||||
let fuse = null;
|
||||
|
||||
/**
|
||||
* Initialize locations database
|
||||
* @returns {Object} Fuse instance for fuzzy search
|
||||
*/
|
||||
function initLocations() {
|
||||
if (!fuse) {
|
||||
if (fs.existsSync(locationsPath)) {
|
||||
locations = JSON.parse(fs.readFileSync(locationsPath, "utf-8"));
|
||||
}
|
||||
fuse = new Fuse(locations, {
|
||||
keys: ["label", "parentLabel"],
|
||||
threshold: 0.3,
|
||||
});
|
||||
}
|
||||
return fuse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Fuse instance for location search
|
||||
* @returns {Object} Fuse instance
|
||||
*/
|
||||
export function getLocationsFuse() {
|
||||
return initLocations();
|
||||
}
|
||||
|
||||
export default { getLocationsFuse };
|
||||
159
scripts/monitor_linkedin.js
Normal file
159
scripts/monitor_linkedin.js
Normal file
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Monitor LinkedIn sync status and notify when complete.
|
||||
* Polls every 2 minutes until LinkedIn URL is available.
|
||||
*
|
||||
* Usage:
|
||||
* node monitor_linkedin.js --jobId "xxx" --channel "webchat"
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// Fuku AI API endpoints (third-party job posting relay service)
|
||||
const API_URL_LK_CHECK = "https://hapi.fuku.ai/hr/rc/anon/job/status/linkedin";
|
||||
|
||||
function sanitizeChannel(channel) {
|
||||
// Only allow alphanumeric characters and hyphens/underscores to prevent prompt injection
|
||||
const sanitized = (channel || "").replace(/[^a-zA-Z0-9_-]/g, "");
|
||||
return sanitized || "webchat";
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const result = {
|
||||
jobId: "",
|
||||
channel: "webchat",
|
||||
intervalMs: 120000, // 2 minutes default
|
||||
maxAttempts: 15, // ~30 minutes total
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const next = args[i + 1];
|
||||
|
||||
switch (arg) {
|
||||
case "--jobId":
|
||||
result.jobId = next;
|
||||
i++;
|
||||
break;
|
||||
case "--channel":
|
||||
result.channel = sanitizeChannel(next);
|
||||
i++;
|
||||
break;
|
||||
case "--interval":
|
||||
result.intervalMs = parseInt(next, 10);
|
||||
i++;
|
||||
break;
|
||||
case "--maxAttempts":
|
||||
result.maxAttempts = parseInt(next, 10);
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateJobId(jobId) {
|
||||
if (!jobId || typeof jobId !== "string") {
|
||||
return false;
|
||||
}
|
||||
// Only allow alphanumeric characters and hyphens (prevent shell injection)
|
||||
return /^[a-zA-Z0-9-]+$/.test(jobId);
|
||||
}
|
||||
|
||||
async function checkLinkedInStatus(jobId) {
|
||||
try {
|
||||
// Fuku AI client identifier (embedded for free tier access)
|
||||
const NUMBER = "job-Z4nV8cQ1LmT7XpR2bH9sJdK6WyEaF0";
|
||||
const response = await axios.post(
|
||||
API_URL_LK_CHECK,
|
||||
{ jobId: jobId },
|
||||
{
|
||||
params: { uid: "1873977344885133312" },
|
||||
headers: { "X-NUMBER": NUMBER },
|
||||
},
|
||||
);
|
||||
|
||||
const jobData = response.data;
|
||||
|
||||
if (jobData.code !== 0) {
|
||||
return { success: false, status: jobData.desc, url: null };
|
||||
}
|
||||
|
||||
if (jobData.data && jobData.data.linkedinUrl) {
|
||||
return { success: true, status: "live", url: jobData.data.linkedinUrl };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
status: jobData.data?.status || "Pending",
|
||||
url: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, status: `Error: ${error.message}`, url: null };
|
||||
}
|
||||
}
|
||||
|
||||
async function notifyUser(message, channel) {
|
||||
// For now, output the message - the parent session will handle delivery
|
||||
console.log(`[NOTIFY:${channel}] ${message}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.jobId) {
|
||||
console.error("❌ Error: --jobId is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!validateJobId(args.jobId)) {
|
||||
console.error(
|
||||
"❌ Error: Invalid jobId format (only alphanumeric and hyphens allowed)",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`🔍 Starting LinkedIn sync monitor for Job ID: ${args.jobId}`);
|
||||
console.log(` Channel: ${args.channel}`);
|
||||
console.log(
|
||||
` Interval: ${args.intervalMs / 1000}s | Max attempts: ${args.maxAttempts}`,
|
||||
);
|
||||
console.log();
|
||||
|
||||
for (let attempt = 1; attempt <= args.maxAttempts; attempt++) {
|
||||
const result = await checkLinkedInStatus(args.jobId);
|
||||
|
||||
if (result.success && result.url) {
|
||||
const message = `🎉 **LinkedIn Sync Complete!**\n\nJob ID: \`${args.jobId}\`\n\nPosition URL: ${result.url}`;
|
||||
await notifyUser(message, args.channel);
|
||||
console.log(`✅ Success after ${attempt} attempts!`);
|
||||
console.log(`URL: ${result.url}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Attempt ${attempt}/${args.maxAttempts}: ⏳ Status: ${result.status}`,
|
||||
);
|
||||
|
||||
if (attempt < args.maxAttempts) {
|
||||
console.log(
|
||||
` Waiting ${args.intervalMs / 1000} seconds before next check...\n`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, args.intervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
const timeoutMsg = `⏰ **LinkedIn Sync Timeout**\n\nAfter ${args.maxAttempts} attempts, the sync status is still "${result.status}".\nJob ID: \`${args.jobId}\`\n\nYou can check manually or try again later.`;
|
||||
await notifyUser(timeoutMsg, args.channel);
|
||||
console.log(`⏰ Timeout after ${args.maxAttempts} attempts.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("❌ Fatal error:", error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
389
scripts/post_job.js
Normal file
389
scripts/post_job.js
Normal file
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Post job openings via Fuku AI API and generate resume collection links.
|
||||
* Uses AI model to generate professional job descriptions.
|
||||
*
|
||||
* Usage:
|
||||
* node post_job.js --title "Senior Frontend Engineer" --city "Singapore" --level "senior"
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import dayjs from "dayjs";
|
||||
import { getLocationsFuse } from "./loadLocations.js";
|
||||
|
||||
// ============================================================================
|
||||
// ⚠️ INTERNAL API PROTECTION
|
||||
// These functions are for internal use ONLY. Always use post_job() as entry.
|
||||
// ============================================================================
|
||||
const INTERNAL_CALL_TOKEN = Symbol("internal-call-token");
|
||||
|
||||
function validateInternalCall(caller, fnName) {
|
||||
if (caller !== INTERNAL_CALL_TOKEN) {
|
||||
throw new Error(
|
||||
`❌ ${fnName}() is an internal function. Use post_job() to post jobs.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// ============================================================================
|
||||
|
||||
// Get Fuse instance for location fuzzy search (loaded from separate module)
|
||||
const fuse = getLocationsFuse();
|
||||
/**
|
||||
* Parse command line arguments
|
||||
*/
|
||||
function parseArgs(args) {
|
||||
const result = {
|
||||
title: "",
|
||||
city: "",
|
||||
description: "",
|
||||
company: "",
|
||||
email: "",
|
||||
linkedinCompanyUrl:
|
||||
"https://www.linkedin.com/company/110195078/admin/dashboard",
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const next = args[i + 1];
|
||||
|
||||
switch (arg) {
|
||||
case "--title":
|
||||
result.title = next;
|
||||
i++;
|
||||
break;
|
||||
case "--city":
|
||||
result.city = next;
|
||||
i++;
|
||||
break;
|
||||
case "--description":
|
||||
result.description = next;
|
||||
i++;
|
||||
break;
|
||||
case "--company":
|
||||
result.company = next;
|
||||
i++;
|
||||
break;
|
||||
case "--email":
|
||||
result.email = next;
|
||||
i++;
|
||||
break;
|
||||
case "--linkedinCompanyUrl":
|
||||
result.linkedinCompanyUrl = next;
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// API Configuration
|
||||
// Fuku AI client identifier (embedded for free tier access)
|
||||
const NUMBER = "job-Z4nV8cQ1LmT7XpR2bH9sJdK6WyEaF0";
|
||||
|
||||
const API_URL_GEN = "https://hapi.fuku.ai/hr/rc/anon/job/upload";
|
||||
const API_URL = "https://hapi.fuku.ai/hr/rc/anon/job/create";
|
||||
const API_URL_LK = "https://hapi.fuku.ai/hr/rc/anon/job/sync/linkedin";
|
||||
const API_URL_LK_CHECK = "https://hapi.fuku.ai/hr/rc/anon/job/status/linkedin";
|
||||
|
||||
function validateCredentials() {}
|
||||
|
||||
/**
|
||||
* Check the status of a LinkedIn job posting
|
||||
*
|
||||
* @param {Object} args - Arguments
|
||||
* @param {string} args.jobId - The ID of the job to check
|
||||
* @returns {Promise<string>} Status message or LinkedIn URL
|
||||
*/
|
||||
export async function check_linkedin_status(args) {
|
||||
const { jobId } = args;
|
||||
validateCredentials();
|
||||
try {
|
||||
const response = await axios.post(
|
||||
API_URL_LK_CHECK,
|
||||
{ jobId: jobId },
|
||||
{
|
||||
params: {
|
||||
uid: "1873977344885133312",
|
||||
},
|
||||
headers: {
|
||||
"X-NUMBER": NUMBER,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const jobData = response.data;
|
||||
|
||||
if (jobData.code !== 0) {
|
||||
return `❌ **Failed to get LinkedIn job state:** ${jobData.desc}`;
|
||||
}
|
||||
|
||||
if (jobData.data && jobData.data.linkedinUrl) {
|
||||
return `✅ **LinkedIn Job is Live!**\n\nURL: ${jobData.data.linkedinUrl}`;
|
||||
} else {
|
||||
return `⏳ **LinkedIn Sync is still in progress.**\n\nStatus: ${jobData.data?.status || "Pending"}\nPlease check again in 5-10 minutes.`;
|
||||
}
|
||||
} catch (error) {
|
||||
return `❌ **Error checking LinkedIn status:** ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-check LinkedIn status with polling until URL is available
|
||||
*
|
||||
* @param {Object} args - Arguments
|
||||
* @param {string} args.jobId - The ID of the job to check
|
||||
* @param {number} [args.intervalMs=60000] - Polling interval in ms (default: 1 minute)
|
||||
* @param {number} [args.maxAttempts=20] - Maximum number of attempts (default: 20)
|
||||
* @returns {Promise<string>} Status message or LinkedIn URL
|
||||
*/
|
||||
async function getLinkedinState(jobId) {
|
||||
return check_linkedin_status({ jobId });
|
||||
}
|
||||
|
||||
async function postToLinkd(data, linkedinCompanyUrl, token) {
|
||||
validateInternalCall(token, "postToLinkd");
|
||||
validateCredentials();
|
||||
let extra = null;
|
||||
try {
|
||||
extra = JSON.parse(data.extra ?? "");
|
||||
} catch (error) {}
|
||||
const job = {
|
||||
title: data.title,
|
||||
reference: `fuku-${data.id}`,
|
||||
description: data.description,
|
||||
jobId: data.id,
|
||||
linkedinCompanyUrl:
|
||||
linkedinCompanyUrl ||
|
||||
"https://www.linkedin.com/company/110195078/admin/dashboard/",
|
||||
location: extra.location,
|
||||
sublocation: extra.sublocation,
|
||||
cityname: extra.cityname,
|
||||
salarymin: 1,
|
||||
salarymax: 1,
|
||||
app_url: `https://app.fuku.ai/career/apply?id=${data.id}`,
|
||||
salaryper: "month",
|
||||
currency: "USD",
|
||||
jobtype: "2",
|
||||
job_time: "2",
|
||||
startdate: dayjs().format("YYYY-MM-DD"),
|
||||
};
|
||||
// console.log("Linkedin Job Post:", job);
|
||||
|
||||
const res = await axios.post(API_URL_LK, job, {
|
||||
params: {
|
||||
uid: "1873977344885133312",
|
||||
},
|
||||
headers: {
|
||||
"X-NUMBER": NUMBER,
|
||||
},
|
||||
});
|
||||
// console.log("Linkedin Job Post Response:", res.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize job description before sending to external AI service
|
||||
* Removes potential prompt injection patterns
|
||||
*/
|
||||
function sanitizeDescription(desc) {
|
||||
if (!desc || typeof desc !== "string") return "";
|
||||
|
||||
// Remove patterns commonly used for prompt injection
|
||||
let sanitized = desc
|
||||
.replace(/```[\s\S]*?```/g, "") // Remove code blocks
|
||||
.replace(/<\|.*?\|>/g, "") // Remove special markers like <|endoftext|>
|
||||
.replace(
|
||||
/(?:^|\n)\s*(ignore|forget|override|system|instruction|new instruction)[\s\S]{0,200}/gi,
|
||||
"",
|
||||
) // Remove injection attempts
|
||||
.replace(
|
||||
/(?:^|\n)\s*[-*]\s*(ignore|forget|override|system|instruction)[\s\S]{0,200}/gi,
|
||||
"",
|
||||
) // Remove bullet-point injections
|
||||
.trim();
|
||||
|
||||
// Limit length to prevent buffer-based attacks
|
||||
return sanitized.slice(0, 10000);
|
||||
}
|
||||
|
||||
async function genJD(description, token) {
|
||||
validateInternalCall(token, "genJD");
|
||||
validateCredentials();
|
||||
|
||||
// Sanitize description before sending to external AI service
|
||||
const sanitizedDescription = sanitizeDescription(description);
|
||||
if (!sanitizedDescription) {
|
||||
return "❌ **Invalid job description:** Description is empty or contains invalid content.";
|
||||
}
|
||||
|
||||
const body = {
|
||||
content: sanitizedDescription,
|
||||
};
|
||||
try {
|
||||
const response = await axios.post(API_URL_GEN, body, {
|
||||
params: {
|
||||
uid: "1873977344885133312",
|
||||
},
|
||||
headers: {
|
||||
"X-NUMBER": NUMBER,
|
||||
},
|
||||
});
|
||||
|
||||
const jobData = response.data;
|
||||
// console.log("Generated Job Data:", jobData);
|
||||
|
||||
if (jobData.code !== 0) {
|
||||
return `❌ **Failed to generate job description:** ${jobData.desc}`;
|
||||
}
|
||||
|
||||
return jobData.data.description;
|
||||
} catch (error) {
|
||||
return `❌ **Error generating job description:** ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post a job opening via Fuku AI API
|
||||
*
|
||||
* @param {Object} args - Job parameters
|
||||
* @param {string} args.title - Job title
|
||||
* @param {string} args.city_query - Location query
|
||||
* @param {string} [args.description] - Job description (auto-generated if not provided)
|
||||
* @param {string} [args.company] - Company name
|
||||
* @returns {Promise<string>} Result message
|
||||
*/
|
||||
export async function post_job(args) {
|
||||
try {
|
||||
validateCredentials();
|
||||
} catch (error) {
|
||||
return `❌ ${error.message}`;
|
||||
}
|
||||
const { title, city_query, description, company, email, linkedinCompanyUrl } =
|
||||
args;
|
||||
|
||||
// Validate required fields
|
||||
if (!email) {
|
||||
return `❌ **Email is required.** Please provide an email address to receive resumes.\n\nExample: --email "hr@company.com"`;
|
||||
}
|
||||
|
||||
// Fuzzy search for location
|
||||
const results = fuse.search(city_query);
|
||||
if (results.length === 0) {
|
||||
return `❌ Sorry, I couldn't find the location: "${city_query}".`;
|
||||
}
|
||||
|
||||
const matched = results[0].item;
|
||||
|
||||
// Build extra data
|
||||
const extra = JSON.stringify({
|
||||
location: matched.parentValue,
|
||||
sublocation: matched.value,
|
||||
cityname: matched.label,
|
||||
});
|
||||
|
||||
const fullDescription = await genJD(description, INTERNAL_CALL_TOKEN);
|
||||
// console.log("Generated Description:", fullDescription);
|
||||
if (fullDescription.startsWith("❌")) {
|
||||
return fullDescription;
|
||||
}
|
||||
|
||||
// Double-check: sanitize the AI-generated description before sending to job posting API
|
||||
const finalDescription = sanitizeDescription(fullDescription);
|
||||
if (!finalDescription) {
|
||||
return "❌ **Invalid job description:** Generated content was filtered as unsafe.";
|
||||
}
|
||||
|
||||
// Build request body
|
||||
const body = {
|
||||
title,
|
||||
description: finalDescription,
|
||||
location: matched.parentLabel,
|
||||
company: company || "FUKU AI",
|
||||
companySearchKeyword: "",
|
||||
extra,
|
||||
isAiInterview: 1,
|
||||
orgId: "1",
|
||||
email: email,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(API_URL, body, {
|
||||
params: {
|
||||
uid: "1873977344885133312",
|
||||
},
|
||||
headers: {
|
||||
"X-NUMBER": NUMBER,
|
||||
},
|
||||
});
|
||||
|
||||
const jobData = response.data;
|
||||
|
||||
if (jobData.code !== 0) {
|
||||
return `❌ **Failed to post job:** ${jobData.desc}`;
|
||||
}
|
||||
await postToLinkd(jobData.data, linkedinCompanyUrl, INTERNAL_CALL_TOKEN);
|
||||
const jobId = jobData.data.id;
|
||||
|
||||
// Note: LinkedIn sync runs in background, returns immediately
|
||||
// User can check status later with check_linkedin_status
|
||||
|
||||
return (
|
||||
`✅ **Job Posted Successfully!**\n\n` +
|
||||
`**Position:** ${title}\n` +
|
||||
`**Location:** ${matched.label}\n` +
|
||||
`**Job ID:** \`${jobId}\`\n` +
|
||||
`**The resume will be sent to:** ${email}\n\n` +
|
||||
`--- \n` +
|
||||
`**LinkedIn Sync:** ⏳ Processing in background (5-30 min). I'll check and notify you when ready!\n\n` +
|
||||
`You can also manually check with: \`check_linkedin_status\` using Job ID \`${jobId}\``
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMsg = error.response?.data?.message || error.message;
|
||||
return `❌ **Failed to post job:** ${errorMsg}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function for CLI usage
|
||||
*/
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.title || !args.city) {
|
||||
console.error("Error: --title and --city are required");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!args.email) {
|
||||
console.error("Error: --email is required");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!args.description) {
|
||||
console.error("Error: --description is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await post_job({
|
||||
title: args.title,
|
||||
city_query: args.city,
|
||||
description: args.description,
|
||||
company: args.company,
|
||||
email: args.email,
|
||||
linkedinCompanyUrl: args.linkedinCompanyUrl,
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
} catch (error) {
|
||||
console.error("❌ Unexpected error:", error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (process.argv[1] && process.argv[1].includes("post_job.js")) {
|
||||
main().catch((error) => {
|
||||
console.error("❌ Fatal error:", error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user