Sync: 1 new posts from Links App (2026-04-08 11:47)

This commit is contained in:
hermes
2026-04-08 11:47:59 +10:00
parent 297dd54850
commit b183a5628f
3 changed files with 213 additions and 37 deletions
+143 -36
View File
@@ -8,12 +8,12 @@ Usage:
""" """
import urllib.request import urllib.request
import urllib.parse
import json import json
import os import os
import sys import sys
import re import re
import subprocess import subprocess
import shutil
from datetime import datetime from datetime import datetime
# Config # Config
@@ -21,6 +21,15 @@ WIKI_DIR = os.path.expanduser("~/junv/one-knowledge")
LINKS_API = "http://go/api/posts" LINKS_API = "http://go/api/posts"
SYNC_STATE_FILE = os.path.join(WIKI_DIR, ".sync-state") SYNC_STATE_FILE = os.path.join(WIKI_DIR, ".sync-state")
POSTS_DIR = os.path.join(WIKI_DIR, "links-posts") 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(): def get_last_sync_time():
"""Get the last sync timestamp from state file.""" """Get the last sync timestamp from state file."""
@@ -36,7 +45,6 @@ def save_last_sync_time(timestamp):
def slugify(title, max_len=50): def slugify(title, max_len=50):
"""Convert title to a valid filename slug.""" """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'[^\w\s\u4e00-\u9fff-]', '', title)
slug = re.sub(r'[-\s]+', '-', slug) slug = re.sub(r'[-\s]+', '-', slug)
slug = slug.lower().strip('-')[:max_len] 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: with urllib.request.urlopen(url, timeout=30) as resp:
data = json.loads(resp.read().decode('utf-8')) data = json.loads(resp.read().decode('utf-8'))
except Exception as e: except Exception as e:
print(f"Error fetching {url}: {e}") print(f"Error: {e}")
break break
results = data.get('results', []) results = data.get('results', [])
@@ -64,7 +72,6 @@ def fetch_posts_since(since_timestamp=None, limit=100):
print("no results, stopping") print("no results, stopping")
break break
# Filter by timestamp if provided
if since_timestamp: if since_timestamp:
for post in results: for post in results:
if post['created_at'] > since_timestamp: if post['created_at'] > since_timestamp:
@@ -76,14 +83,11 @@ def fetch_posts_since(since_timestamp=None, limit=100):
else: else:
posts.extend(results) posts.extend(results)
# Check if there are more pages
if not data.get('next') or stop: if not data.get('next') or stop:
print("no more pages" if not stop else "") print("no more pages" if not stop else "")
break break
page += 1 page += 1
# Safety limit
if page > 500: if page > 500:
print("safety limit reached") print("safety limit reached")
break break
@@ -95,12 +99,11 @@ def post_to_markdown(post):
post_id = post['id'] post_id = post['id']
title = post.get('title', 'Untitled') title = post.get('title', 'Untitled')
content = post.get('content', post.get('summary', '')) 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] updated = post.get('updated_at', '')[:10]
tags = [t['name'] for t in post.get('tag_details', [])] tags = [t['name'] for t in post.get('tag_details', [])]
external_url = f"http://go/ui/posts/{post_id}/" external_url = f"http://go/ui/posts/{post_id}/"
# Build frontmatter
safe_title = title.replace('"', '\\"') safe_title = title.replace('"', '\\"')
frontmatter = f"""--- frontmatter = f"""---
title: "{safe_title}" title: "{safe_title}"
@@ -115,17 +118,125 @@ external: {external_url}
return frontmatter 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(): def main():
dry_run = '--dry-run' in sys.argv 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 # Get last sync time
last_sync = get_last_sync_time() last_sync = get_last_sync_time()
if last_sync: print(f"\n--- Step 1: Fetch new posts ---")
print(f"Last sync: {last_sync}") print(f"Last sync: {last_sync or 'First sync'}")
else:
print("First sync - will fetch all posts")
# Fetch posts # Fetch posts
posts = fetch_posts_since(last_sync) posts = fetch_posts_since(last_sync)
@@ -133,10 +244,14 @@ def main():
if not posts: if not posts:
print("No new posts to sync") 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 return
if dry_run: 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]: for p in posts[:5]:
print(f" - {p['title']}") print(f" - {p['title']}")
if len(posts) > 5: if len(posts) > 5:
@@ -148,6 +263,7 @@ def main():
# Write posts # Write posts
newest_timestamp = None newest_timestamp = None
print(f"\n--- Step 2: Write {len(posts)} posts ---")
for post in posts: for post in posts:
post_id = post['id'] post_id = post['id']
title = post.get('title', 'Untitled') title = post.get('title', 'Untitled')
@@ -155,47 +271,38 @@ def main():
filename = f"{post_id}-{slug}.md" filename = f"{post_id}-{slug}.md"
filepath = os.path.join(POSTS_DIR, filename) filepath = os.path.join(POSTS_DIR, filename)
# Write markdown
with open(filepath, 'w', encoding='utf-8') as f: with open(filepath, 'w', encoding='utf-8') as f:
f.write(post_to_markdown(post)) f.write(post_to_markdown(post))
# Track newest timestamp
created = post.get('created_at', '') created = post.get('created_at', '')
if not newest_timestamp or created > newest_timestamp: if not newest_timestamp or created > newest_timestamp:
newest_timestamp = created newest_timestamp = created
print(f" Wrote: {filename}") print(f" {filename}")
# Update sync state # Update sync state
if newest_timestamp: if newest_timestamp:
save_last_sync_time(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 # Git operations
print("Committing and pushing to GitHub...") print("\n--- Step 3: Commit and push ---")
os.chdir(WIKI_DIR) run_cmd("git add links-posts/ .scripts/sync-links-to-wiki.py .sync-state")
# Check git config result = run_cmd("git status --porcelain")
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)
if result.stdout.strip(): if result.stdout.strip():
commit_msg = f"Sync: {len(posts)} new posts from Links App ({datetime.now().strftime('%Y-%m-%d %H:%M')})" 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) run_cmd(f"git commit -m '{commit_msg}'")
push_result = subprocess.run(["git", "push"], capture_output=True, text=True) print(f"📝 Committed: {commit_msg}")
if push_result.returncode == 0:
print(f"✅ Pushed: {commit_msg}") if git_push():
print(f"\n✅ SUCCESS: Synced {len(posts)} posts!")
else: else:
print(f"⚠️ Push failed: {push_result.stderr}") print(f"\n Push failed - posts are saved locally")
else: else:
print("No changes to commit") print("No changes to commit")
print(f"[{datetime.now().isoformat()}] Sync complete!") print(f"\n[{datetime.now().isoformat()}] 🎉 Sync complete!")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+1 -1
View File
@@ -1 +1 @@
2026-04-08T10:05:43.651150+10:00 2026-04-08T11:47:09.031011+10:00
@@ -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,0008,000 | ABN/Pty Ltd 运营 |
| 职业责任险 PI Insurance | ~$2,0004,000 | IT咨询必备 |
| 设备、软件、培训 | ~$3,0005,000 | 可抵税 |
| **扣除后应税收入** | ~**$225,000230,000** | 视结构而定 |
### 税负(个人 ABNFY2024/25
$225,000 的个人税约为:**~$78,00082,000**(含 Medicare Levy 2%
**税后到手:约 $145,000150,000**
> 如果成立 **Pty Ltd** 公司结构,可以通过合理分红、工资组合降低税率,税后可能多留 $10,000–20,000。
---
## 对比同等 Full-time Employee
$1,200/天的合同工,对应的 full-time equivalent 大约是这样理解:
| 对比维度 | 合同工 ($1,200/天) | Full-time Employee |
|---------|------------------|-------------------|
| **税前年收入** | $240,000200天) | 对应约 **$180,000200,000** base |
| **Super** | ❌ 自己缴(11.5%~$27,000) | ✅ 雇主付(不占工资) |
| **年假/病假** | ❌ 无薪 | ✅ 4周年假 + 10天病假 |
| **公共假日** | ❌ 不上班不赚钱 | ✅ 带薪 |
| **稳定性** | 合同结束有空档风险 | 稳定 |
| **税务复杂度** | 高(需会计) | 低 |
| **税后实得(估算)** | ~$145,000155,000 | ~$125,000135,000$185k base |
---
## 结论
$1,200/天的合同工,**实际等效的 full-time base salary 大约在 $180,000200,000 之间**才算"打平"(考虑 super、假期、稳定性等 hidden benefits)。
税后实得合同工会**略多 $15,000–25,000**,但要承担空档风险、自己管税务和 super、以及压力。
一般经验法则:**合同日薪 ÷ 1.3 × 220 ≈ 对等的 full-time package**$1,200 ÷ 1.3 × 220 ≈ **$203,000 package**,这个数字挺准的。