mirror of
https://github.com/wahyd4/one-knowledge.git
synced 2026-08-08 20:59:08 +10:00
Sync: 1 new posts from Links App (2026-04-08 11:47)
This commit is contained in:
+143
-36
@@ -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
@@ -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,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**,这个数字挺准的。
|
||||||
Reference in New Issue
Block a user