diff --git a/.scripts/sync-links-to-wiki.py b/.scripts/sync-links-to-wiki.py index 016fd27..1fe68f2 100755 --- a/.scripts/sync-links-to-wiki.py +++ b/.scripts/sync-links-to-wiki.py @@ -8,12 +8,12 @@ Usage: """ import urllib.request -import urllib.parse import json import os import sys import re import subprocess +import shutil from datetime import datetime # Config @@ -21,6 +21,15 @@ WIKI_DIR = os.path.expanduser("~/junv/one-knowledge") LINKS_API = "http://go/api/posts" SYNC_STATE_FILE = os.path.join(WIKI_DIR, ".sync-state") POSTS_DIR = os.path.join(WIKI_DIR, "links-posts") +SCRIPT_DIR = os.path.join(WIKI_DIR, ".scripts") + +def run_cmd(cmd, cwd=None, capture=True): + """Run a shell command and return result.""" + result = subprocess.run( + cmd, shell=True, cwd=cwd or WIKI_DIR, + capture_output=capture, text=True + ) + return result def get_last_sync_time(): """Get the last sync timestamp from state file.""" @@ -36,7 +45,6 @@ def save_last_sync_time(timestamp): def slugify(title, max_len=50): """Convert title to a valid filename slug.""" - # Remove special characters, keep Chinese and alphanumeric slug = re.sub(r'[^\w\s\u4e00-\u9fff-]', '', title) slug = re.sub(r'[-\s]+', '-', slug) slug = slug.lower().strip('-')[:max_len] @@ -56,7 +64,7 @@ def fetch_posts_since(since_timestamp=None, limit=100): with urllib.request.urlopen(url, timeout=30) as resp: data = json.loads(resp.read().decode('utf-8')) except Exception as e: - print(f"Error fetching {url}: {e}") + print(f"Error: {e}") break results = data.get('results', []) @@ -64,7 +72,6 @@ def fetch_posts_since(since_timestamp=None, limit=100): print("no results, stopping") break - # Filter by timestamp if provided if since_timestamp: for post in results: if post['created_at'] > since_timestamp: @@ -76,14 +83,11 @@ def fetch_posts_since(since_timestamp=None, limit=100): else: posts.extend(results) - # Check if there are more pages if not data.get('next') or stop: print("no more pages" if not stop else "") break page += 1 - - # Safety limit if page > 500: print("safety limit reached") break @@ -95,12 +99,11 @@ def post_to_markdown(post): post_id = post['id'] title = post.get('title', 'Untitled') content = post.get('content', post.get('summary', '')) - created = post.get('created_at', '')[:10] # Just the date + created = post.get('created_at', '')[:10] updated = post.get('updated_at', '')[:10] tags = [t['name'] for t in post.get('tag_details', [])] external_url = f"http://go/ui/posts/{post_id}/" - # Build frontmatter safe_title = title.replace('"', '\\"') frontmatter = f"""--- title: "{safe_title}" @@ -115,17 +118,125 @@ external: {external_url} return frontmatter +def git_pull_with_rebase(): + """Pull with rebase to keep history clean, handle conflicts.""" + print("🔄 Git pull (rebase)...") + + # First check if there are remote changes + result = run_cmd("git fetch origin") + if result.returncode != 0: + print(f"⚠️ Fetch failed: {result.stderr}") + return False + + # Check status + result = run_cmd("git status -sb") + branch_info = result.stdout.strip() + + # Check if we need to pull + if "ahead" in branch_info and "behind" not in branch_info: + print("✅ No remote changes, nothing to pull") + return True + + # Try rebase first + result = run_cmd("git rebase origin/master") + if result.returncode == 0: + print("✅ Rebase successful") + return True + + # Conflict during rebase - handle it + print("⚠️ Conflict detected during rebase!") + print(f"Conflict output: {result.stdout}") + print(f"Conflict stderr: {result.stderr}") + + # Check which files have conflicts + result = run_cmd("git diff --name-only --diff-filter=U") + conflicted_files = result.stdout.strip().split('\n') if result.stdout.strip() else [] + + if not conflicted_files or conflicted_files == ['']: + print("No conflicting files found, trying abort") + run_cmd("git rebase --abort") + return False + + print(f"📁 Conflicted files: {conflicted_files}") + + # Strategy: For our use case, we prefer wiki content + # So we'll use --ours for conflicts in links-posts/ and --theirs for others + for f in conflicted_files: + if not f: + continue + if f.startswith('links-posts/'): + # Prefer our new posts + print(f" → Keeping our version: {f}") + run_cmd(f"git checkout --ours {f}") + else: + # For other files, prefer their version (user might have edited on Mac) + print(f" → Keeping their version: {f}") + run_cmd(f"git checkout --theirs {f}") + + # Stage resolved files + run_cmd("git add -A") + + # Continue rebase + result = run_cmd("git rebase --continue") + if result.returncode == 0: + print("✅ Conflict resolved and rebase continued") + return True + else: + print(f"⚠️ Could not continue rebase: {result.stderr}") + # If still failing, abort and just overwrite + print("🔄 Aborting rebase, will use force push...") + run_cmd("git rebase --abort") + return False + +def git_push(): + """Push to remote.""" + print("📤 Pushing to GitHub...") + + # Try normal push first + result = run_cmd("git push") + if result.returncode == 0: + print("✅ Push successful") + return True + + # Push failed - might need force or pull first + print(f"⚠️ Push failed: {result.stderr}") + + # Try pull first then push + if git_pull_with_rebase(): + result = run_cmd("git push") + if result.returncode == 0: + print("✅ Push successful after pull") + return True + + # Last resort: force push (dangerous but sometimes necessary) + print("⚠️ Force pushing...") + result = run_cmd("git push --force-with-lease") + if result.returncode == 0: + print("✅ Force push successful") + return True + + print(f"❌ Push failed: {result.stderr}") + return False + def main(): dry_run = '--dry-run' in sys.argv - print(f"[{datetime.now().isoformat()}] Starting Links App sync...") + print(f"\n[{datetime.now().isoformat()}] 🚀 Starting Links App sync...") + os.chdir(WIKI_DIR) + + # Git setup + run_cmd("git config user.name 'hermes'") + run_cmd("git config user.email 'hermes@junv'") + + # Pull first to get any changes from Mac + if not dry_run: + print("\n--- Step 0: Sync with remote ---") + git_pull_with_rebase() # Get last sync time last_sync = get_last_sync_time() - if last_sync: - print(f"Last sync: {last_sync}") - else: - print("First sync - will fetch all posts") + print(f"\n--- Step 1: Fetch new posts ---") + print(f"Last sync: {last_sync or 'First sync'}") # Fetch posts posts = fetch_posts_since(last_sync) @@ -133,10 +244,14 @@ def main(): if not posts: print("No new posts to sync") + # Still do a push to sync any other changes + if not dry_run: + print("\n--- Step 3: Sync other changes ---") + git_push() return if dry_run: - print(f"[DRY RUN] Would sync {len(posts)} posts:") + print(f"\n[DRY RUN] Would sync {len(posts)} posts:") for p in posts[:5]: print(f" - {p['title']}") if len(posts) > 5: @@ -148,6 +263,7 @@ def main(): # Write posts newest_timestamp = None + print(f"\n--- Step 2: Write {len(posts)} posts ---") for post in posts: post_id = post['id'] title = post.get('title', 'Untitled') @@ -155,47 +271,38 @@ def main(): filename = f"{post_id}-{slug}.md" filepath = os.path.join(POSTS_DIR, filename) - # Write markdown with open(filepath, 'w', encoding='utf-8') as f: f.write(post_to_markdown(post)) - # Track newest timestamp created = post.get('created_at', '') if not newest_timestamp or created > newest_timestamp: newest_timestamp = created - print(f" Wrote: {filename}") + print(f" ✅ {filename}") # Update sync state if newest_timestamp: save_last_sync_time(newest_timestamp) - print(f"Updated sync state to {newest_timestamp}") + print(f"\nUpdated sync state to {newest_timestamp}") # Git operations - print("Committing and pushing to GitHub...") - os.chdir(WIKI_DIR) + print("\n--- Step 3: Commit and push ---") + run_cmd("git add links-posts/ .scripts/sync-links-to-wiki.py .sync-state") - # Check git config - subprocess.run(["git", "config", "user.name", "hermes"], capture_output=True) - subprocess.run(["git", "config", "user.email", "hermes@junv"], capture_output=True) - - # Add, commit, push - subprocess.run(["git", "add", "links-posts/"], capture_output=True) - subprocess.run(["git", "add", ".sync-state"], capture_output=True) - - result = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True) + result = run_cmd("git status --porcelain") if result.stdout.strip(): commit_msg = f"Sync: {len(posts)} new posts from Links App ({datetime.now().strftime('%Y-%m-%d %H:%M')})" - subprocess.run(["git", "commit", "-m", commit_msg], capture_output=True) - push_result = subprocess.run(["git", "push"], capture_output=True, text=True) - if push_result.returncode == 0: - print(f"✅ Pushed: {commit_msg}") + run_cmd(f"git commit -m '{commit_msg}'") + print(f"📝 Committed: {commit_msg}") + + if git_push(): + print(f"\n✅ SUCCESS: Synced {len(posts)} posts!") else: - print(f"⚠️ Push failed: {push_result.stderr}") + print(f"\n❌ Push failed - posts are saved locally") else: print("No changes to commit") - print(f"[{datetime.now().isoformat()}] Sync complete!") + print(f"\n[{datetime.now().isoformat()}] 🎉 Sync complete!") if __name__ == "__main__": main() diff --git a/.sync-state b/.sync-state index dd4854b..62063fd 100644 --- a/.sync-state +++ b/.sync-state @@ -1 +1 @@ -2026-04-08T10:05:43.651150+10:00 \ No newline at end of file +2026-04-08T11:47:09.031011+10:00 \ No newline at end of file diff --git a/links-posts/2377-深度剖析日薪1200合同工的实际年收入税负估算及其与全职员工薪资待遇的全面对比.md b/links-posts/2377-深度剖析日薪1200合同工的实际年收入税负估算及其与全职员工薪资待遇的全面对比.md new file mode 100644 index 0000000..010f92f --- /dev/null +++ b/links-posts/2377-深度剖析日薪1200合同工的实际年收入税负估算及其与全职员工薪资待遇的全面对比.md @@ -0,0 +1,69 @@ +--- +title: "深度剖析日薪$1,200合同工的实际年收入、税负估算及其与全职员工薪资待遇的全面对比" +created: 2026-04-08 +updated: 2026-04-08 +type: summary +tags: [contract, finance, notes, salary, telegram] +external: http://go/ui/posts/2377/ +--- + +我来帮你算一下。 + +## 基本假设 + +- 日薪:$1,200/天 +- 工作天数:约 220 天/年(52周 × 5天 - 公共假日约10天 - 预留一些空档期) +- 实际可能更保守:200 天(考虑项目间空档、找合同的空窗期) + +--- + +## 合同工收入计算 + +### 毛收入 +| 场景 | 工作天数 | 年毛收入 | +|------|---------|---------| +| 乐观(少空档) | 220天 | **$264,000** | +| 现实(有空档) | 200天 | **$240,000** | +| 保守(新手/市场差) | 180天 | **$216,000** | + +### 扣除项(以现实 $240,000 为基准) + +| 项目 | 金额 | 说明 | +|------|------|------| +| 公司运营成本(会计、保险、注册等) | ~$5,000–8,000 | ABN/Pty Ltd 运营 | +| 职业责任险 PI Insurance | ~$2,000–4,000 | IT咨询必备 | +| 设备、软件、培训 | ~$3,000–5,000 | 可抵税 | +| **扣除后应税收入** | ~**$225,000–230,000** | 视结构而定 | + +### 税负(个人 ABN,FY2024/25) +$225,000 的个人税约为:**~$78,000–82,000**(含 Medicare Levy 2%) + +**税后到手:约 $145,000–150,000** + +> 如果成立 **Pty Ltd** 公司结构,可以通过合理分红、工资组合降低税率,税后可能多留 $10,000–20,000。 + +--- + +## 对比同等 Full-time Employee + +$1,200/天的合同工,对应的 full-time equivalent 大约是这样理解: + +| 对比维度 | 合同工 ($1,200/天) | Full-time Employee | +|---------|------------------|-------------------| +| **税前年收入** | $240,000(200天) | 对应约 **$180,000–200,000** base | +| **Super** | ❌ 自己缴(11.5%,~$27,000) | ✅ 雇主付(不占工资) | +| **年假/病假** | ❌ 无薪 | ✅ 4周年假 + 10天病假 | +| **公共假日** | ❌ 不上班不赚钱 | ✅ 带薪 | +| **稳定性** | 合同结束有空档风险 | 稳定 | +| **税务复杂度** | 高(需会计) | 低 | +| **税后实得(估算)** | ~$145,000–155,000 | ~$125,000–135,000($185k base) | + +--- + +## 结论 + +$1,200/天的合同工,**实际等效的 full-time base salary 大约在 $180,000–200,000 之间**才算"打平"(考虑 super、假期、稳定性等 hidden benefits)。 + +税后实得合同工会**略多 $15,000–25,000**,但要承担空档风险、自己管税务和 super、以及压力。 + +一般经验法则:**合同日薪 ÷ 1.3 × 220 ≈ 对等的 full-time package**,$1,200 ÷ 1.3 × 220 ≈ **$203,000 package**,这个数字挺准的。 \ No newline at end of file